gitlab-mail-room-0.0.9/0000755000175100017510000000000014036627661013715 5ustar pravipravigitlab-mail-room-0.0.9/.gitignore0000644000175100017510000000023514036627661015705 0ustar pravipravi*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp loggitlab-mail-room-0.0.9/Rakefile0000644000175100017510000000016514036627661015364 0ustar pravipravirequire "bundler/gem_tasks" require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) task :default => :spec gitlab-mail-room-0.0.9/spec/0000755000175100017510000000000014036627661014647 5ustar pravipravigitlab-mail-room-0.0.9/spec/fixtures/0000755000175100017510000000000014036627661016520 5ustar pravipravigitlab-mail-room-0.0.9/spec/fixtures/test_config.yml0000644000175100017510000000065714036627661021557 0ustar pravipravi--- :health_check: :address: "127.0.0.1" :port: 8080 :mailboxes: - :email: "user1@gmail.com" :password: "password" :name: "inbox" :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" :logger: :log_path: "logfile.log" - :email: "user2@gmail.com" :password: "password" :name: "inbox" :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" gitlab-mail-room-0.0.9/spec/lib/0000755000175100017510000000000014036627661015415 5ustar pravipravigitlab-mail-room-0.0.9/spec/lib/coordinator_spec.rb0000644000175100017510000000425314036627661021303 0ustar pravipravirequire 'spec_helper' describe MailRoom::Coordinator do describe '#initialize' do it 'builds a watcher for each mailbox' do MailRoom::MailboxWatcher.expects(:new).with('mailbox1').returns('watcher1') MailRoom::MailboxWatcher.expects(:new).with('mailbox2').returns('watcher2') coordinator = MailRoom::Coordinator.new(['mailbox1', 'mailbox2']) expect(coordinator.watchers).to eq(['watcher1', 'watcher2']) end it 'makes no watchers when mailboxes is empty' do coordinator = MailRoom::Coordinator.new([]) expect(coordinator.watchers).to eq([]) end it 'sets the health check' do health_check = MailRoom::HealthCheck.new({ address: '127.0.0.1', port: 8080}) coordinator = MailRoom::Coordinator.new([], health_check) expect(coordinator.health_check).to eq(health_check) end end describe '#run' do it 'runs each watcher' do watcher = stub watcher.stubs(:run) watcher.stubs(:quit) health_check = stub health_check.stubs(:run) health_check.stubs(:quit) MailRoom::MailboxWatcher.stubs(:new).returns(watcher) coordinator = MailRoom::Coordinator.new(['mailbox1'], health_check) coordinator.stubs(:sleep_while_running) watcher.expects(:run) watcher.expects(:quit) health_check.expects(:run) health_check.expects(:quit) coordinator.run end it 'should go to sleep after running watchers' do coordinator = MailRoom::Coordinator.new([]) coordinator.stubs(:running=) coordinator.stubs(:running?).returns(false) coordinator.expects(:running=).with(true) coordinator.expects(:running?) coordinator.run end it 'should set attribute running to true' do coordinator = MailRoom::Coordinator.new([]) coordinator.stubs(:sleep_while_running) coordinator.run expect(coordinator.running).to eq(true) end end describe '#quit' do it 'quits each watcher' do watcher = stub(:quit) MailRoom::MailboxWatcher.stubs(:new).returns(watcher) coordinator = MailRoom::Coordinator.new(['mailbox1']) watcher.expects(:quit) coordinator.quit end end end gitlab-mail-room-0.0.9/spec/lib/message_spec.rb0000644000175100017510000000143714036627661020405 0ustar pravipravi# frozen_string_literal:true require 'spec_helper' require 'securerandom' describe MailRoom::Message do let(:uid) { SecureRandom.hex } let(:body) { 'hello world' } subject { described_class.new(uid: uid, body: body) } describe '#initalize' do it 'initializes with required parameters' do subject expect(subject.uid).to eq(uid) expect(subject.body).to eq(body) end end describe '#==' do let(:dup) { described_class.new(uid: uid, body: body) } it 'matches an equivalent message' do expect(dup == subject).to be true end it 'does not match a message with a different UID' do msg = described_class.new(uid: '12345', body: body) expect(subject == msg).to be false expect(msg == subject).to be false end end end gitlab-mail-room-0.0.9/spec/lib/mailbox_watcher_spec.rb0000644000175100017510000000300114036627661022116 0ustar pravipravirequire 'spec_helper' describe MailRoom::MailboxWatcher do let(:mailbox) {build_mailbox} describe '#running?' do it 'is false by default' do watcher = MailRoom::MailboxWatcher.new(mailbox) expect(watcher.running?).to eq(false) end end describe '#run' do let(:imap) {stub(:login => true, :select => true)} let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)} before :each do Net::IMAP.stubs(:new).returns(imap) # prevent connection end it 'loops over wait while running' do connection = MailRoom::IMAP::Connection.new(mailbox) MailRoom::IMAP::Connection.stubs(:new).returns(connection) watcher.expects(:running?).twice.returns(true, false) connection.expects(:wait).once connection.expects(:on_new_message).once watcher.run watcher.watching_thread.join # wait for finishing run end end describe '#quit' do let(:imap) {stub(:login => true, :select => true)} let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)} before :each do Net::IMAP.stubs(:new).returns(imap) # prevent connection end it 'closes and waits for the connection' do connection = MailRoom::IMAP::Connection.new(mailbox) connection.stubs(:wait) connection.stubs(:quit) MailRoom::IMAP::Connection.stubs(:new).returns(connection) watcher.run expect(watcher.running?).to eq(true) connection.expects(:quit) watcher.quit expect(watcher.running?).to eq(false) end end end gitlab-mail-room-0.0.9/spec/lib/delivery/0000755000175100017510000000000014036627661017240 5ustar pravipravigitlab-mail-room-0.0.9/spec/lib/delivery/letter_opener_spec.rb0000644000175100017510000000202114036627661023441 0ustar pravipravirequire 'spec_helper' require 'mail_room/delivery/letter_opener' describe MailRoom::Delivery::LetterOpener do describe '#deliver' do let(:mailbox) {build_mailbox(:location => '/tmp/somewhere')} let(:delivery_method) {stub(:deliver!)} let(:mail) {stub} before :each do Mail.stubs(:read_from_string).returns(mail) ::LetterOpener::DeliveryMethod.stubs(:new).returns(delivery_method) end it 'creates a new LetterOpener::DeliveryMethod' do ::LetterOpener::DeliveryMethod.expects(:new).with(:location => '/tmp/somewhere').returns(delivery_method) MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message') end it 'parses the message string with Mail' do ::Mail.expects(:read_from_string).with('a message') MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message') end it 'delivers the mail message' do delivery_method.expects(:deliver!).with(mail) MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message') end end endgitlab-mail-room-0.0.9/spec/lib/delivery/sidekiq_spec.rb0000644000175100017510000000424014036627661022230 0ustar pravipravirequire 'spec_helper' require 'mail_room/delivery/sidekiq' describe MailRoom::Delivery::Sidekiq do subject { described_class.new(options) } let(:redis) { subject.send(:client) } let(:options) { MailRoom::Delivery::Sidekiq::Options.new(mailbox) } describe '#options' do let(:redis_url) { 'redis://localhost' } context 'when only redis_url is specified' do let(:mailbox) { build_mailbox( delivery_method: :sidekiq, delivery_options: { redis_url: redis_url } ) } it 'client has same specified redis_url' do expect(redis.client.options[:url]).to eq(redis_url) end it 'client is a instance of RedisNamespace class' do expect(redis).to be_a ::Redis end end context 'when namespace is specified' do let(:namespace) { 'sidekiq_mailman' } let(:mailbox) { build_mailbox( delivery_method: :sidekiq, delivery_options: { redis_url: redis_url, namespace: namespace } ) } it 'client has same specified namespace' do expect(redis.namespace).to eq(namespace) end it 'client is a instance of RedisNamespace class' do expect(redis).to be_a ::Redis::Namespace end end context 'when sentinel is specified' do let(:redis_url) { 'redis://:mypassword@sentinel-master:6379' } let(:sentinels) { [{ host: '10.0.0.1', port: '26379' }] } let(:mailbox) { build_mailbox( delivery_method: :sidekiq, delivery_options: { redis_url: redis_url, sentinels: sentinels } ) } before { ::Redis::Client::Connector::Sentinel.any_instance.stubs(:resolve).returns(sentinels) } it 'client has same specified sentinel params' do expect(redis.client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel expect(redis.client.options[:host]).to eq('sentinel-master') expect(redis.client.options[:password]).to eq('mypassword') expect(redis.client.options[:sentinels]).to eq(sentinels) end end end end gitlab-mail-room-0.0.9/spec/lib/delivery/logger_spec.rb0000644000175100017510000000204314036627661022055 0ustar pravipravirequire 'spec_helper' require 'mail_room/delivery/logger' describe MailRoom::Delivery::Logger do describe '#initialize' do context "without a log path" do let(:mailbox) {build_mailbox} it 'creates a new ruby logger' do ::Logger.stubs(:new) ::Logger.expects(:new).with(STDOUT) MailRoom::Delivery::Logger.new(mailbox) end end context "with a log path" do let(:mailbox) {build_mailbox(:log_path => '/var/log/mail-room.log')} it 'creates a new file to append to' do file = stub(:sync=) File.expects(:open).with('/var/log/mail-room.log', 'a').returns(file) ::Logger.stubs(:new).with(file) MailRoom::Delivery::Logger.new(mailbox) end end end describe '#deliver' do let(:mailbox) {build_mailbox} it 'writes the message to info' do logger = stub(:info) ::Logger.stubs(:new).returns(logger) logger.expects(:info).with('a message') MailRoom::Delivery::Logger.new(mailbox).deliver('a message') end end end gitlab-mail-room-0.0.9/spec/lib/delivery/que_spec.rb0000644000175100017510000000200114036627661021362 0ustar pravipravirequire 'spec_helper' require 'mail_room/delivery/que' describe MailRoom::Delivery::Que do describe '#deliver' do let(:mailbox) {build_mailbox({ delivery_options: { database: 'delivery_test', username: 'postgres', password: '', queue: 'default', priority: 5, job_class: 'ParseMailJob' } })} let(:connection) {stub} let(:options) {MailRoom::Delivery::Que::Options.new(mailbox)} it 'stores the message in que_jobs table' do PG.expects(:connect).with({ host: 'localhost', port: 5432, dbname: 'delivery_test', user: 'postgres', password: '' }).returns(connection) connection.expects(:exec).with( "INSERT INTO que_jobs (priority, job_class, queue, args) VALUES ($1, $2, $3, $4)", [ 5, 'ParseMailJob', 'default', JSON.dump(['email']) ] ) MailRoom::Delivery::Que.new(options).deliver('email') end end end gitlab-mail-room-0.0.9/spec/lib/delivery/postback_spec.rb0000644000175100017510000000522614036627661022412 0ustar pravipravirequire 'spec_helper' require 'mail_room/delivery/postback' describe MailRoom::Delivery::Postback do describe '#deliver' do context 'with token auth delivery' do let(:mailbox) {build_mailbox({ :delivery_url => 'http://localhost/inbox', :delivery_token => 'abcdefg' })} let(:delivery_options) { MailRoom::Delivery::Postback::Options.new(mailbox) } it 'posts the message with faraday' do connection = stub request = stub Faraday.stubs(:new).returns(connection) connection.expects(:token_auth).with('abcdefg') connection.expects(:post).yields(request) request.expects(:url).with('http://localhost/inbox') request.expects(:body=).with('a message') MailRoom::Delivery::Postback.new(delivery_options).deliver('a message') end end context 'with basic auth delivery options' do let(:mailbox) {build_mailbox({ :delivery_options => { :url => 'http://localhost/inbox', :username => 'user1', :password => 'password123abc' } })} let(:delivery_options) { MailRoom::Delivery::Postback::Options.new(mailbox) } it 'posts the message with faraday' do connection = stub request = stub Faraday.stubs(:new).returns(connection) connection.expects(:basic_auth).with('user1', 'password123abc') connection.expects(:post).yields(request) request.expects(:url).with('http://localhost/inbox') request.expects(:body=).with('a message') MailRoom::Delivery::Postback.new(delivery_options).deliver('a message') end context 'with content type in the delivery options' do let(:mailbox) {build_mailbox({ :delivery_options => { :url => 'http://localhost/inbox', :username => 'user1', :password => 'password123abc', :content_type => 'text/plain' } })} let(:delivery_options) { MailRoom::Delivery::Postback::Options.new(mailbox) } it 'posts the message with faraday' do connection = stub request = stub Faraday.stubs(:new).returns(connection) connection.expects(:post).yields(request) request.stubs(:url) request.stubs(:body=) request.stubs(:headers).returns({}) connection.expects(:basic_auth).with('user1', 'password123abc') MailRoom::Delivery::Postback.new(delivery_options).deliver('a message') expect(request.headers['Content-Type']).to eq('text/plain') end end end end end gitlab-mail-room-0.0.9/spec/lib/arbitration/0000755000175100017510000000000014036627661017733 5ustar pravipravigitlab-mail-room-0.0.9/spec/lib/arbitration/redis_spec.rb0000644000175100017510000000726414036627661022411 0ustar pravipravirequire 'spec_helper' require 'mail_room/arbitration/redis' describe MailRoom::Arbitration::Redis do let(:mailbox) { build_mailbox( arbitration_options: { namespace: "mail_room", redis_url: ENV['REDIS_URL'] } ) } let(:options) { described_class::Options.new(mailbox) } subject { described_class.new(options) } # Private, but we don't care. let(:redis) { subject.send(:client) } describe '#deliver?' do context "when called the first time" do after do redis.del("delivered:123") end it "returns true" do expect(subject.deliver?(123)).to be_truthy end it "increments the delivered flag" do subject.deliver?(123) expect(redis.get("delivered:123")).to eq("1") end it "sets an expiration on the delivered flag" do subject.deliver?(123) expect(redis.ttl("delivered:123")).to be > 0 end end context "when called the second time" do before do #Short expiration, 1 second, for testing subject.deliver?(123, 1) end after do redis.del("delivered:123") end it "returns false" do expect(subject.deliver?(123, 1)).to be_falsey end it "after expiration returns true" do # Fails locally because fakeredis returns 0, not false expect(subject.deliver?(123, 1)).to be_falsey sleep(redis.ttl("delivered:123")+1) expect(subject.deliver?(123, 1)).to be_truthy end end context "when called for another uid" do before do subject.deliver?(123) end after do redis.del("delivered:123") redis.del("delivered:124") end it "returns true" do expect(subject.deliver?(124)).to be_truthy end end end context 'redis client connection params' do context 'when only url is present' do let(:redis_url) { ENV.fetch('REDIS_URL', 'redis://localhost:6379') } let(:mailbox) { build_mailbox( arbitration_options: { redis_url: redis_url } ) } after do redis.del("delivered:123") end it 'client has same specified url' do subject.deliver?(123) expect(redis.client.options[:url]).to eq redis_url end it 'client is a instance of Redis class' do expect(redis).to be_a Redis end end context 'when namespace is present' do let(:namespace) { 'mail_room' } let(:mailbox) { build_mailbox( arbitration_options: { namespace: namespace } ) } it 'client has same specified namespace' do expect(redis.namespace).to eq(namespace) end it 'client is a instance of RedisNamespace class' do expect(redis).to be_a ::Redis::Namespace end end context 'when sentinel is present' do let(:redis_url) { 'redis://:mypassword@sentinel-master:6379' } let(:sentinels) { [{ host: '10.0.0.1', port: '26379' }] } let(:mailbox) { build_mailbox( arbitration_options: { redis_url: redis_url, sentinels: sentinels } ) } before { ::Redis::Client::Connector::Sentinel.any_instance.stubs(:resolve).returns(sentinels) } it 'client has same specified sentinel params' do expect(redis.client.instance_variable_get(:@connector)).to be_a Redis::Client::Connector::Sentinel expect(redis.client.options[:host]).to eq('sentinel-master') expect(redis.client.options[:password]).to eq('mypassword') expect(redis.client.options[:sentinels]).to eq(sentinels) end end end end gitlab-mail-room-0.0.9/spec/lib/logger/0000755000175100017510000000000014036627661016674 5ustar pravipravigitlab-mail-room-0.0.9/spec/lib/logger/structured_spec.rb0000644000175100017510000000251314036627661022440 0ustar pravipravirequire 'spec_helper' describe MailRoom::Logger::Structured do subject { described_class.new $stdout } let!(:now) { Time.now } let(:message) { { action: 'exciting development', message: 'testing 123' } } before do Time.stubs(:now).returns(now) end [:debug, :info, :warn, :error, :fatal].each do |level| it "logs #{level}" do expect { subject.send(level, message) }.to output(json_matching(level.to_s.upcase, message)).to_stdout_from_any_process end end it 'logs unknown' do expect { subject.unknown(message) }.to output(json_matching("ANY", message)).to_stdout_from_any_process end it 'only accepts hashes' do expect { subject.unknown("just a string!") }.to raise_error(ArgumentError, /must be a Hash/) end context 'logging a hash as a message' do it 'merges the contents' do input = { additional_field: "some value" } expected = { severity: 'DEBUG', time: now, additional_field: "some value" } expect { subject.debug(input) }.to output(as_regex(expected)).to_stdout_from_any_process end end def json_matching(level, message) contents = { severity: level, time: now }.merge(message) as_regex(contents) end def as_regex(contents) /#{Regexp.quote(contents.to_json)}/ end end gitlab-mail-room-0.0.9/spec/lib/crash_handler_spec.rb0000644000175100017510000000175714036627661021563 0ustar pravipravirequire 'spec_helper' describe MailRoom::CrashHandler do let(:error_message) { "oh noes!" } let(:error) { RuntimeError.new(error_message) } let(:stdout) { StringIO.new } describe '#handle' do subject{ described_class.new(stdout).handle(error, format) } context 'when given a json format' do let(:format) { 'json' } it 'writes a json message to stdout' do subject stdout.rewind output = stdout.read expect(output).to end_with("\n") expect(JSON.parse(output)['message']).to eq(error_message) end end context 'when given a blank format' do let(:format) { "" } it 'raises an error as designed' do expect{ subject }.to raise_error(error.class, error_message) end end context 'when given a nonexistent format' do let(:format) { "nonsense" } it 'raises an error as designed' do expect{ subject }.to raise_error(error.class, error_message) end end end end gitlab-mail-room-0.0.9/spec/lib/imap/0000755000175100017510000000000014036627661016343 5ustar pravipravigitlab-mail-room-0.0.9/spec/lib/imap/message_spec.rb0000644000175100017510000000160014036627661021323 0ustar pravipravi# frozen_string_literal:true require 'spec_helper' require 'securerandom' describe MailRoom::IMAP::Message do let(:uid) { SecureRandom.hex } let(:body) { 'hello world' } let(:seqno) { 5 } subject { described_class.new(uid: uid, body: body, seqno: seqno) } describe '#initalize' do it 'initializes with required parameters' do subject expect(subject.uid).to eq(uid) expect(subject.body).to eq(body) expect(subject.seqno).to eq(seqno) end end describe '#==' do let(:dup) { described_class.new(uid: uid, body: body, seqno: seqno) } let(:base_msg) { MailRoom::Message.new(uid: uid, body: body) } it 'matches an equivalent message' do expect(dup == subject).to be true end it 'does not match a base message' do expect(subject == base_msg).to be false expect(base_msg == subject).to be false end end end gitlab-mail-room-0.0.9/spec/lib/imap/connection_spec.rb0000644000175100017510000000312414036627661022041 0ustar pravipravirequire 'spec_helper' describe MailRoom::IMAP::Connection do let(:imap) {stub} let(:mailbox) {build_mailbox(delete_after_delivery: true, expunge_deleted: true)} before :each do Net::IMAP.stubs(:new).returns(imap) end context "with imap set up" do let(:connection) {MailRoom::IMAP::Connection.new(mailbox)} let(:uid) { 1 } let(:seqno) { 8 } before :each do imap.stubs(:starttls) imap.stubs(:login) imap.stubs(:select) end it "is logged in" do expect(connection.logged_in?).to eq(true) end it "is not idling" do expect(connection.idling?).to eq(false) end it "is not disconnected" do imap.stubs(:disconnected?).returns(false) expect(connection.disconnected?).to eq(false) end it "is ready to idle" do expect(connection.ready_to_idle?).to eq(true) end it "waits for a message to process" do new_message = MailRoom::IMAP::Message.new(uid: uid, body: 'a message', seqno: seqno) connection.on_new_message do |message| expect(message).to eq(new_message) true end attr = { 'UID' => uid, 'RFC822' => new_message.body } fetch_data = Net::IMAP::FetchData.new(seqno, attr) imap.expects(:idle) imap.stubs(:uid_search).with(mailbox.search_command).returns([], [uid]) imap.expects(:uid_fetch).with([uid], "RFC822").returns([fetch_data]) mailbox.expects(:deliver?).with(uid).returns(true) imap.expects(:store).with(seqno, "+FLAGS", [Net::IMAP::DELETED]) imap.expects(:expunge).once connection.wait end end end gitlab-mail-room-0.0.9/spec/lib/mailbox_spec.rb0000644000175100017510000001001114036627661020400 0ustar pravipravirequire 'spec_helper' describe MailRoom::Mailbox do let(:sample_message) { MailRoom::Message.new(uid: 123, body: 'a message') } describe "#deliver" do context "with arbitration_method of noop" do it 'arbitrates with a Noop instance' do mailbox = build_mailbox({:arbitration_method => 'noop'}) noop = stub(:deliver?) MailRoom::Arbitration['noop'].stubs(:new => noop) uid = 123 noop.expects(:deliver?).with(uid) mailbox.deliver?(uid) end end context "with arbitration_method of redis" do it 'arbitrates with a Redis instance' do mailbox = build_mailbox({:arbitration_method => 'redis'}) redis = stub(:deliver?) MailRoom::Arbitration['redis'].stubs(:new => redis) uid = 123 redis.expects(:deliver?).with(uid) mailbox.deliver?(uid) end end context "with delivery_method of noop" do it 'delivers with a Noop instance' do mailbox = build_mailbox({:delivery_method => 'noop'}) noop = stub(:deliver) MailRoom::Delivery['noop'].stubs(:new => noop) noop.expects(:deliver).with(sample_message.body) mailbox.deliver(sample_message) end end context "with delivery_method of logger" do it 'delivers with a Logger instance' do mailbox = build_mailbox({:delivery_method => 'logger'}) logger = stub(:deliver) MailRoom::Delivery['logger'].stubs(:new => logger) logger.expects(:deliver).with(sample_message.body) mailbox.deliver(sample_message) end end context "with delivery_method of postback" do it 'delivers with a Postback instance' do mailbox = build_mailbox({:delivery_method => 'postback'}) postback = stub(:deliver) MailRoom::Delivery['postback'].stubs(:new => postback) postback.expects(:deliver).with(sample_message.body) mailbox.deliver(sample_message) end end context "with delivery_method of letter_opener" do it 'delivers with a LetterOpener instance' do mailbox = build_mailbox({:delivery_method => 'letter_opener'}) letter_opener = stub(:deliver) MailRoom::Delivery['letter_opener'].stubs(:new => letter_opener) letter_opener.expects(:deliver).with(sample_message.body) mailbox.deliver(sample_message) end end context "without an RFC822 attribute" do it "doesn't deliver the message" do mailbox = build_mailbox({ name: "magic mailbox", delivery_method: 'noop' }) noop = stub(:deliver) MailRoom::Delivery['noop'].stubs(:new => noop) noop.expects(:deliver).never mailbox.deliver(MailRoom::Message.new(uid: 1234, body: nil)) end end context "with ssl options hash" do it 'replaces verify mode with constant' do mailbox = build_mailbox({:ssl => {:verify_mode => :none}}) expect(mailbox.ssl_options).to eq({:verify_mode => OpenSSL::SSL::VERIFY_NONE}) end end context 'structured logger setup' do it 'sets up the logger correctly and does not error' do mailbox = build_mailbox({ name: "magic mailbox", logger: { log_path: '/dev/null' } }) expect{ mailbox.logger.info(message: "asdf") }.not_to raise_error end it 'accepts stdout symbol to mean STDOUT' do mailbox = build_mailbox({ name: "magic mailbox", logger: { log_path: :stdout } }) expect{ mailbox.logger.info(message: "asdf") }.not_to raise_error end it 'sets up the noop logger correctly and does not error' do mailbox = build_mailbox({ name: "magic mailbox" }) expect{ mailbox.logger.info(message: "asdf") }.not_to raise_error end end end describe "#validate!" do context "with missing configuration" do it 'raises an error' do expect { build_mailbox({:name => nil}) }.to raise_error(MailRoom::ConfigurationError) expect { build_mailbox({:host => nil}) }.to raise_error(MailRoom::ConfigurationError) end end end end gitlab-mail-room-0.0.9/spec/lib/cli_spec.rb0000644000175100017510000000411114036627661017520 0ustar pravipravirequire 'spec_helper' describe MailRoom::CLI do let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))} let!(:configuration) {MailRoom::Configuration.new({:config_path => config_path})} let(:coordinator) {stub(:run => true, :quit => true)} let(:configuration_args) { anything } let(:coordinator_args) { [anything, anything] } describe '.new' do let(:args) {["-c", "a path"]} before :each do MailRoom::Configuration.expects(:new).with(configuration_args).returns(configuration) MailRoom::Coordinator.stubs(:new).with(*coordinator_args).returns(coordinator) end context 'with configuration args' do let(:configuration_args) do {:config_path => 'a path'} end it 'parses arguments into configuration' do expect(MailRoom::CLI.new(args).configuration).to eq configuration end end context 'with coordinator args' do let(:coordinator_args) do [configuration.mailboxes, anything] end it 'creates a new coordinator with configuration' do expect(MailRoom::CLI.new(args).coordinator).to eq(coordinator) end end end describe '#start' do let(:cli) {MailRoom::CLI.new([])} before :each do cli.configuration = configuration cli.coordinator = coordinator cli.stubs(:exit) end it 'starts running the coordinator' do coordinator.expects(:run) cli.start end context 'on error' do let(:error) { RuntimeError.new("oh noes!") } let(:coordinator) { stub(run: true, quit: true) } let(:crash_handler) { stub(handle: nil) } before do cli.instance_variable_set(:@options, {exit_error_format: error_format}) coordinator.stubs(:run).raises(error) MailRoom::CrashHandler.stubs(:new).returns(crash_handler) end context 'json format provided' do let(:error_format) { 'json' } it 'passes onto CrashHandler' do crash_handler.expects(:handle).with(error, error_format) cli.start end end end end end gitlab-mail-room-0.0.9/spec/lib/configuration_spec.rb0000644000175100017510000000202214036627661021617 0ustar pravipravirequire 'spec_helper' describe MailRoom::Configuration do let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))} describe '#initalize' do context 'with config_path' do let(:configuration) { MailRoom::Configuration.new(:config_path => config_path) } it 'parses yaml into mailbox objects' do MailRoom::Mailbox.stubs(:new).returns('mailbox1', 'mailbox2') expect(configuration.mailboxes).to eq(['mailbox1', 'mailbox2']) end it 'parses health check' do expect(configuration.health_check).to be_a(MailRoom::HealthCheck) end end context 'without config_path' do let(:configuration) { MailRoom::Configuration.new } it 'sets mailboxes to an empty set' do MailRoom::Mailbox.stubs(:new) MailRoom::Mailbox.expects(:new).never expect(configuration.mailboxes).to eq([]) end it 'sets the health check to nil' do expect(configuration.health_check).to be_nil end end end end gitlab-mail-room-0.0.9/spec/lib/health_check_spec.rb0000644000175100017510000000236314036627661021362 0ustar pravipravi# frozen_string_literal: true require 'spec_helper' describe MailRoom::HealthCheck do let(:address) { '127.0.0.1' } let(:port) { 8000 } let(:params) { { address: address, port: port } } subject { described_class.new(params) } describe '#initialize' do context 'with valid parameters' do it 'validates successfully' do expect(subject).to be_a(described_class) end end context 'with invalid address' do let(:address) { nil } it 'raises an error' do expect { subject }.to raise_error('No health check address specified') end end context 'with invalid port' do let(:port) { nil } it 'raises an error' do expect { subject }.to raise_error('Health check port 0 is invalid') end end end describe '#run' do it 'sets running to true' do server = stub(start: true) subject.stubs(:create_server).returns(server) subject.run expect(subject.running).to be true end end describe '#quit' do it 'sets running to false' do server = stub(start: true, shutdown: true) subject.stubs(:create_server).returns(server) subject.run subject.quit expect(subject.running).to be false end end end gitlab-mail-room-0.0.9/spec/spec_helper.rb0000644000175100017510000000136514036627661017472 0ustar pravipravirequire 'simplecov' SimpleCov.start require 'bundler/setup' require 'rspec' require 'mocha/api' require File.expand_path('../../lib/mail_room', __FILE__) RSpec.configure do |config| config.mock_with :mocha config.run_all_when_everything_filtered = true config.filter_run :focus # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = 'random' end REQUIRED_MAILBOX_DEFAULTS = { :name => "inbox", :email => "user@example.com", :password => "password123" } def build_mailbox(options = {}) MailRoom::Mailbox.new(REQUIRED_MAILBOX_DEFAULTS.merge(options)) end gitlab-mail-room-0.0.9/.ruby-version0000644000175100017510000000000614036627661016356 0ustar pravipravi2.7.2 gitlab-mail-room-0.0.9/bin/0000755000175100017510000000000014036627661014465 5ustar pravipravigitlab-mail-room-0.0.9/bin/mail_room0000755000175100017510000000011014036627661016361 0ustar pravipravi#!/usr/bin/env ruby require 'mail_room' MailRoom::CLI.new(ARGV).start gitlab-mail-room-0.0.9/CHANGELOG.md0000644000175100017510000000524214036627661015531 0ustar pravipravi## mail_room 0.10.0 ## * Remove imap backports * Increase minimum ruby version to 2.3 * Postback basic_auth support - PR#92 * Docs for ActionMailbox - PR#92 * Configuration option for delivery_klass - PR#93 * Expunge deleted - PR#90 * Raise error on a few fields of missing configuration - PR#89 * Remove fakeredis gem - PR#87 *Tony Pitale <@tpitale>* * Fix redis arbitration to use NX+EX - PR#86 *Craig Miskell <@craigmiskell-gitlab>* * Structured (JSON) logger - PR#88 *charlie <@cablett>* ## mail_room 0.9.1 ## * __FILE__ support in yml ERb config - PR#80 *Gabriel Mazetto <@brodock>* ## mail_room 0.9.0 ## * Redis Sentinel configuration support - PR#79 *Gabriel Mazetto <@brodock>* ## mail_room 0.8.1 ## * Check watching thread exists before joining - PR#78 *Michal Galet <@galet>* ## mail_room 0.8.0 ## * Rework the mailbox watcher and handler into a new Connection class to abstract away IMAP handling details *Tony Pitale <@tpitale>* ## mail_room 0.7.0 ## * Backports idle timeout from ruby 2.3.0 * Sets default to 29 minutes to prevent IMAP disconnects * Validates that the timeout does not exceed 29 minutes *Tony Pitale <@tpitale>* ## mail_room 0.6.1 ## * ERB parsing of configuration yml file to enable using ENV variables *Douwe Maan <@DouweM>* ## mail_room 0.6.0 ## * Add redis Arbitration to reduce multiple deliveries of the same message when running multiple MailRoom instances on the same inbox *Douwe Maan <@DouweM>* ## mail_room 0.5.2 ## * Fix Sidekiq delivery method for non-UTF8 email *Douwe Maan <@DouweM>* * Add StartTLS session support *Tony Pitale <@tpitale>* ## mail_room 0.5.1 ## * Re-idle after 29 minutes to maintain IDLE connection *Douwe Maan <@DouweM>* ## mail_room 0.5.0 ## * Que delivery method *Tony Pitale <@tpitale>* ## mail_room 0.4.2 ## * rescue from all IOErrors, not just EOFError *Douwe Maan <@DouweM>* ## mail_room 0.4.1 ## * Fix redis default host/port configuration * Mailbox does not attempt delivery without a message *Douwe Maan <@DouweM>* ## mail_room 0.4.0 ## * Sidekiq delivery method * Option to delete messages after delivered *Douwe Maan <@DouweM>* * -q/--quiet do not raise errors on missing configuration * prefetch mail messages before idling * delivery-method-specific delivery options configuration *Tony Pitale <@tpitale>* ## mail_room 0.3.1 ## * Rescue from EOFError and re-setup mailroom *Tony Pitale <@tpitale>* ## mail_room 0.3.0 ## * Reconnect and idle if disconnected during an existing idle. * Set idling thread to abort on exception so any unhandled exceptions will stop mail_room running. *Tony Pitale <@tpitale>* gitlab-mail-room-0.0.9/mail_room.gemspec0000644000175100017510000000264014036627661017242 0ustar pravipravi# -*- encoding: utf-8 -*- lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'mail_room/version' Gem::Specification.new do |gem| gem.name = "gitlab-mail_room" gem.version = MailRoom::VERSION gem.authors = ["Tony Pitale"] gem.email = ["tpitale@gmail.com"] gem.description = %q{mail_room will proxy email (gmail) from IMAP to a delivery method} gem.summary = %q{mail_room will proxy email (gmail) from IMAP to a callback URL, logger, or letter_opener} gem.homepage = "http://github.com/tpitale/mail_room" gem.files = `git ls-files`.split($/) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_development_dependency "rake" gem.add_development_dependency "rspec", "~> 3.9" gem.add_development_dependency "mocha", "~> 1.11" gem.add_development_dependency "simplecov" gem.add_development_dependency "webrick", "~> 1.6" # for testing delivery methods gem.add_development_dependency "faraday" gem.add_development_dependency "mail" gem.add_development_dependency "letter_opener" gem.add_development_dependency "redis", "~> 3.3.1" gem.add_development_dependency "redis-namespace" gem.add_development_dependency "pg" gem.add_development_dependency "charlock_holmes" end gitlab-mail-room-0.0.9/lib/0000755000175100017510000000000014036627661014463 5ustar pravipravigitlab-mail-room-0.0.9/lib/mail_room/0000755000175100017510000000000014036627661016441 5ustar pravipravigitlab-mail-room-0.0.9/lib/mail_room/arbitration.rb0000644000175100017510000000037714036627661021313 0ustar pravipravimodule MailRoom module Arbitration def [](name) require_relative("./arbitration/#{name}") case name when "redis" Arbitration::Redis else Arbitration::Noop end end module_function :[] end end gitlab-mail-room-0.0.9/lib/mail_room/imap.rb0000644000175100017510000000025614036627661017717 0ustar pravipravi# frozen_string_literal: true module MailRoom module IMAP autoload :Connection, 'mail_room/imap/connection' autoload :Message, 'mail_room/imap/message' end end gitlab-mail-room-0.0.9/lib/mail_room/configuration.rb0000644000175100017510000000245014036627661021636 0ustar pravipravirequire "erb" module MailRoom # Wraps configuration for a set of individual mailboxes with global config # @author Tony Pitale class Configuration attr_accessor :mailboxes, :log_path, :quiet, :health_check # Initialize a new configuration of mailboxes def initialize(options={}) self.mailboxes = [] self.quiet = options.fetch(:quiet, false) if options.has_key?(:config_path) begin erb = ERB.new(File.read(options[:config_path])) erb.filename = options[:config_path] config_file = YAML.load(erb.result) set_mailboxes(config_file[:mailboxes]) set_health_check(config_file[:health_check]) rescue => e raise e unless quiet end end end # Builds individual mailboxes from YAML configuration # # @param mailboxes_config def set_mailboxes(mailboxes_config) mailboxes_config.each do |attributes| self.mailboxes << Mailbox.new(attributes) end end # Builds the health checker from YAML configuration # # @param health_check_config nil or a Hash containing :address and :port def set_health_check(health_check_config) return unless health_check_config self.health_check = HealthCheck.new(health_check_config) end end end gitlab-mail-room-0.0.9/lib/mail_room/delivery.rb0000644000175100017510000000066614036627661020621 0ustar pravipravimodule MailRoom module Delivery def [](name) require_relative("./delivery/#{name}") case name when "postback" Delivery::Postback when "logger" Delivery::Logger when "letter_opener" Delivery::LetterOpener when "sidekiq" Delivery::Sidekiq when "que" Delivery::Que else Delivery::Noop end end module_function :[] end end gitlab-mail-room-0.0.9/lib/mail_room/mailbox.rb0000644000175100017510000001153614036627661020427 0ustar pravipravirequire "mail_room/delivery" require "mail_room/arbitration" require "mail_room/imap" module MailRoom # Mailbox Configuration fields MAILBOX_FIELDS = [ :email, :password, :host, :port, :ssl, :start_tls, :limit_max_unread, #to avoid 'Error in IMAP command UID FETCH: Too long argument' :idle_timeout, :search_command, :name, :delete_after_delivery, :expunge_deleted, :delivery_klass, :delivery_method, # :noop, :logger, :postback, :letter_opener :log_path, # for logger :delivery_url, # for postback :delivery_token, # for postback :content_type, # for postback :location, # for letter_opener :delivery_options, :arbitration_method, :arbitration_options, :logger ] ConfigurationError = Class.new(RuntimeError) IdleTimeoutTooLarge = Class.new(RuntimeError) # Holds configuration for each of the email accounts we wish to monitor # and deliver email to when new emails arrive over imap Mailbox = Struct.new(*MAILBOX_FIELDS) do # Keep it to 29 minutes or less # The IMAP serve will close the connection after 30 minutes of inactivity # (which sending IDLE and then nothing technically is), so we re-idle every # 29 minutes, as suggested by the spec: https://tools.ietf.org/html/rfc2177 IMAP_IDLE_TIMEOUT = 29 * 60 # 29 minutes in in seconds REQUIRED_CONFIGURATION = [:name, :email, :password, :host, :port] # Default attributes for the mailbox configuration DEFAULTS = { :search_command => 'UNSEEN', :delivery_method => 'postback', :host => 'imap.gmail.com', :port => 993, :ssl => true, :start_tls => false, :limit_max_unread => 0, :idle_timeout => IMAP_IDLE_TIMEOUT, :delete_after_delivery => false, :expunge_deleted => false, :delivery_options => {}, :arbitration_method => 'noop', :arbitration_options => {}, :logger => {} } # Store the configuration and require the appropriate delivery method # @param attributes [Hash] configuration options def initialize(attributes={}) super(*DEFAULTS.merge(attributes).values_at(*members)) validate! end def logger @logger ||= case self[:logger] when Logger self[:logger] else self[:logger] ||= {} MailRoom::Logger::Structured.new(normalize_log_path(self[:logger][:log_path])) end end def delivery_klass self[:delivery_klass] ||= Delivery[delivery_method] end def arbitration_klass Arbitration[arbitration_method] end def delivery @delivery ||= delivery_klass.new(parsed_delivery_options) end def arbitrator @arbitrator ||= arbitration_klass.new(parsed_arbitration_options) end def deliver?(uid) logger.info({context: context, uid: uid, action: "asking arbiter to deliver", arbitrator: arbitrator.class.name}) arbitrator.deliver?(uid) end # deliver the email message # @param message [MailRoom::Message] def deliver(message) body = message.body return true unless body logger.info({context: context, uid: message.uid, action: "sending to deliverer", deliverer: delivery.class.name, byte_size: body.bytesize}) delivery.deliver(body) end # true, false, or ssl options hash def ssl_options replace_verify_mode(ssl) end def context { email: self.email, name: self.name } end def validate! if self[:idle_timeout] > IMAP_IDLE_TIMEOUT raise IdleTimeoutTooLarge, "Please use an idle timeout smaller than #{29*60} to prevent " \ "IMAP server disconnects" end REQUIRED_CONFIGURATION.each do |k| if self[k].nil? raise ConfigurationError, "Field :#{k} is required in Mailbox: #{inspect}" end end end private def parsed_arbitration_options arbitration_klass::Options.new(self) end def parsed_delivery_options delivery_klass::Options.new(self) end def replace_verify_mode(options) return options unless options.is_a?(Hash) return options unless options.has_key?(:verify_mode) options[:verify_mode] = lookup_verify_mode(options[:verify_mode]) options end def lookup_verify_mode(verify_mode) case verify_mode.to_sym when :none OpenSSL::SSL::VERIFY_NONE when :peer OpenSSL::SSL::VERIFY_PEER when :client_once OpenSSL::SSL::VERIFY_CLIENT_ONCE when :fail_if_no_peer_cert OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT end end def normalize_log_path(log_path) case log_path when nil, "" nil when :stdout, "STDOUT" STDOUT when :stderr, "STDERR" STDERR else log_path end end end end gitlab-mail-room-0.0.9/lib/mail_room/delivery/0000755000175100017510000000000014036627661020264 5ustar pravipravigitlab-mail-room-0.0.9/lib/mail_room/delivery/letter_opener.rb0000644000175100017510000000164314036627661023464 0ustar pravipravirequire 'erb' require 'mail' require 'letter_opener' module MailRoom module Delivery # LetterOpener Delivery method # @author Tony Pitale class LetterOpener Options = Struct.new(:location) do def initialize(mailbox) location = mailbox.location || mailbox.delivery_options[:location] super(location) end end # Build a new delivery, hold the delivery options # @param [MailRoom::Delivery::LetterOpener::Options] def initialize(delivery_options) @delivery_options = delivery_options end # Trigger `LetterOpener` to deliver our message # @param message [String] the email message as a string, RFC822 format def deliver(message) method = ::LetterOpener::DeliveryMethod.new(:location => @delivery_options.location) method.deliver!(Mail.read_from_string(message)) true end end end end gitlab-mail-room-0.0.9/lib/mail_room/delivery/postback.rb0000644000175100017510000000425014036627661022420 0ustar pravipravirequire 'faraday' module MailRoom module Delivery # Postback Delivery method # @author Tony Pitale class Postback Options = Struct.new(:url, :token, :username, :password, :logger, :content_type) do def initialize(mailbox) url = mailbox.delivery_url || mailbox.delivery_options[:delivery_url] || mailbox.delivery_options[:url] token = mailbox.delivery_token || mailbox.delivery_options[:delivery_token] || mailbox.delivery_options[:token] username = mailbox.delivery_options[:username] password = mailbox.delivery_options[:password] logger = mailbox.logger content_type = mailbox.delivery_options[:content_type] super(url, token, username, password, logger, content_type) end def token_auth? !self[:token].nil? end def basic_auth? !self[:username].nil? && !self[:password].nil? end end # Build a new delivery, hold the delivery options # @param [MailRoom::Delivery::Postback::Options] def initialize(delivery_options) puts delivery_options @delivery_options = delivery_options end # deliver the message using Faraday to the configured delivery_options url # @param message [String] the email message as a string, RFC822 format def deliver(message) connection = Faraday.new if @delivery_options.token_auth? connection.token_auth @delivery_options.token elsif @delivery_options.basic_auth? connection.basic_auth( @delivery_options.username, @delivery_options.password ) end connection.post do |request| request.url @delivery_options.url request.body = message # request.options[:timeout] = 3 request.headers['Content-Type'] = @delivery_options.content_type unless @delivery_options.content_type.nil? end @delivery_options.logger.info({ delivery_method: 'Postback', action: 'message pushed', url: @delivery_options.url }) true end end end end gitlab-mail-room-0.0.9/lib/mail_room/delivery/sidekiq.rb0000644000175100017510000000515214036627661022245 0ustar pravipravirequire "redis" require "securerandom" require "json" require "charlock_holmes" module MailRoom module Delivery # Sidekiq Delivery method # @author Douwe Maan class Sidekiq Options = Struct.new(:redis_url, :namespace, :sentinels, :queue, :worker, :logger) do def initialize(mailbox) redis_url = mailbox.delivery_options[:redis_url] || "redis://localhost:6379" namespace = mailbox.delivery_options[:namespace] sentinels = mailbox.delivery_options[:sentinels] queue = mailbox.delivery_options[:queue] || "default" worker = mailbox.delivery_options[:worker] logger = mailbox.logger super(redis_url, namespace, sentinels, queue, worker, logger) end end attr_accessor :options # Build a new delivery, hold the mailbox configuration # @param [MailRoom::Delivery::Sidekiq::Options] def initialize(options) @options = options end # deliver the message by pushing it onto the configured Sidekiq queue # @param message [String] the email message as a string, RFC822 format def deliver(message) item = item_for(message) client.lpush("queue:#{options.queue}", JSON.generate(item)) @options.logger.info({ delivery_method: 'Sidekiq', action: 'message pushed' }) true end private def client @client ||= begin sentinels = options.sentinels redis_options = { url: options.redis_url } redis_options[:sentinels] = sentinels if sentinels redis = ::Redis.new(redis_options) namespace = options.namespace if namespace require 'redis/namespace' Redis::Namespace.new(namespace, redis: redis) else redis end end end def item_for(message) { 'class' => options.worker, 'args' => [utf8_encode_message(message)], 'queue' => options.queue, 'jid' => SecureRandom.hex(12), 'retry' => false, 'enqueued_at' => Time.now.to_f } end def utf8_encode_message(message) message = message.dup message.force_encoding("UTF-8") return message if message.valid_encoding? detection = CharlockHolmes::EncodingDetector.detect(message) return message unless detection && detection[:encoding] # Convert non-UTF-8 body UTF-8 so it can be dumped as JSON. CharlockHolmes::Converter.convert(message, detection[:encoding], 'UTF-8') end end end end gitlab-mail-room-0.0.9/lib/mail_room/delivery/noop.rb0000644000175100017510000000057014036627661021566 0ustar pravipravimodule MailRoom module Delivery # Noop Delivery method # @author Tony Pitale class Noop Options = Class.new do def initialize(*) super() end end # build a new delivery, do nothing def initialize(*) end # accept the delivery, do nothing def deliver(*) true end end end end gitlab-mail-room-0.0.9/lib/mail_room/delivery/que.rb0000644000175100017510000000372114036627661021406 0ustar pravipravirequire 'pg' require 'json' module MailRoom module Delivery # Que Delivery method # @author Tony Pitale class Que Options = Struct.new(:host, :port, :database, :username, :password, :queue, :priority, :job_class, :logger) do def initialize(mailbox) host = mailbox.delivery_options[:host] || "localhost" port = mailbox.delivery_options[:port] || 5432 database = mailbox.delivery_options[:database] username = mailbox.delivery_options[:username] password = mailbox.delivery_options[:password] queue = mailbox.delivery_options[:queue] || '' priority = mailbox.delivery_options[:priority] || 100 # lowest priority for Que job_class = mailbox.delivery_options[:job_class] logger = mailbox.logger super(host, port, database, username, password, queue, priority, job_class, logger) end end attr_reader :options # Build a new delivery, hold the mailbox configuration # @param [MailRoom::Delivery::Que::Options] def initialize(options) @options = options end # deliver the message by pushing it onto the configured Sidekiq queue # @param message [String] the email message as a string, RFC822 format def deliver(message) queue_job(message) @options.logger.info({ delivery_method: 'Que', action: 'message pushed' }) end private def connection PG.connect(connection_options) end def connection_options { host: options.host, port: options.port, dbname: options.database, user: options.username, password: options.password } end def queue_job(*args) sql = "INSERT INTO que_jobs (priority, job_class, queue, args) VALUES ($1, $2, $3, $4)" connection.exec(sql, [options.priority, options.job_class, options.queue, JSON.dump(args)]) end end end end gitlab-mail-room-0.0.9/lib/mail_room/delivery/logger.rb0000644000175100017510000000165414036627661022076 0ustar pravipravirequire 'logger' module MailRoom module Delivery # File/STDOUT Logger Delivery method # @author Tony Pitale class Logger Options = Struct.new(:log_path) do def initialize(mailbox) log_path = mailbox.log_path || mailbox.delivery_options[:log_path] super(log_path) end end # Build a new delivery, hold the delivery options # open a file or stdout for IO depending on the options # @param [MailRoom::Delivery::Logger::Options] def initialize(delivery_options) io = File.open(delivery_options.log_path, 'a') if delivery_options.log_path io ||= STDOUT io.sync = true @logger = ::Logger.new(io) end # Write the message to our logger # @param message [String] the email message as a string, RFC822 format def deliver(message) @logger.info message true end end end end gitlab-mail-room-0.0.9/lib/mail_room/arbitration/0000755000175100017510000000000014036627661020757 5ustar pravipravigitlab-mail-room-0.0.9/lib/mail_room/arbitration/noop.rb0000644000175100017510000000036614036627661022264 0ustar pravipravimodule MailRoom module Arbitration class Noop Options = Class.new do def initialize(*) super() end end def initialize(*) end def deliver?(*) true end end end end gitlab-mail-room-0.0.9/lib/mail_room/arbitration/redis.rb0000644000175100017510000000331114036627661022410 0ustar pravipravirequire "redis" module MailRoom module Arbitration class Redis Options = Struct.new(:redis_url, :namespace, :sentinels) do def initialize(mailbox) redis_url = mailbox.arbitration_options[:redis_url] || "redis://localhost:6379" namespace = mailbox.arbitration_options[:namespace] sentinels = mailbox.arbitration_options[:sentinels] super(redis_url, namespace, sentinels) end end # Expire after 10 minutes so Redis doesn't get filled up with outdated data. EXPIRATION = 600 attr_accessor :options def initialize(options) @options = options end def deliver?(uid, expiration = EXPIRATION) key = "delivered:#{uid}" # Set the key, but only if it doesn't already exist; # the return value is true if successful, false if the key was already set, # which is conveniently the correct return value for this method # Any subsequent failure in the instance which gets the lock will be dealt # with by the expiration, at which time another instance can pick up the # message and try again. client.set(key, 1, {:nx => true, :ex => expiration}) end private def client @client ||= begin sentinels = options.sentinels redis_options = { url: options.redis_url } redis_options[:sentinels] = sentinels if sentinels redis = ::Redis.new(redis_options) namespace = options.namespace if namespace require 'redis/namespace' ::Redis::Namespace.new(namespace, redis: redis) else redis end end end end end end gitlab-mail-room-0.0.9/lib/mail_room/message.rb0000644000175100017510000000042714036627661020415 0ustar pravipravi# frozen_string_literal: true module MailRoom class Message attr_reader :uid, :body def initialize(uid:, body:) @uid = uid @body = body end def ==(other) self.class == other.class && uid == other.uid && body == other.body end end end gitlab-mail-room-0.0.9/lib/mail_room/coordinator.rb0000644000175100017510000000211114036627661021304 0ustar pravipravimodule MailRoom # Coordinate the mailbox watchers # @author Tony Pitale class Coordinator attr_accessor :watchers, :running, :health_check # build watchers for a set of mailboxes # @params mailboxes [Array] mailboxes to be watched # @params health_check health checker to run def initialize(mailboxes, health_check = nil) self.watchers = [] @health_check = health_check mailboxes.each {|box| self.watchers << MailboxWatcher.new(box)} end alias :running? :running # start each of the watchers to running def run health_check&.run watchers.each(&:run) self.running = true sleep_while_running ensure quit end # quit each of the watchers when we're done running def quit health_check&.quit watchers.each(&:quit) end private # @private def sleep_while_running # do we need to sweep for dead watchers? # or do we let the mailbox rebuild connections while(running?) do; sleep 1; end end end end gitlab-mail-room-0.0.9/lib/mail_room/mailbox_watcher.rb0000644000175100017510000000322014036627661022133 0ustar pravipravirequire "mail_room/connection" module MailRoom # TODO: split up between processing and idling? # Watch a Mailbox # @author Tony Pitale class MailboxWatcher attr_accessor :watching_thread # Watch a new mailbox # @param mailbox [MailRoom::Mailbox] the mailbox to watch def initialize(mailbox) @mailbox = mailbox @running = false @connection = nil end # are we running? # @return [Boolean] def running? @running end # run the mailbox watcher def run @mailbox.logger.info({ context: @mailbox.context, action: "Setting up watcher" }) @running = true connection.on_new_message do |message| @mailbox.deliver(message) end self.watching_thread = Thread.start do while(running?) do connection.wait end end watching_thread.abort_on_exception = true end # stop running, cleanup connection def quit @mailbox.logger.info({ context: @mailbox.context, action: "Quitting connection..." }) @running = false if @connection @connection.quit @connection = nil end @mailbox.logger.info({ context: @mailbox.context, action: "Terminating watching thread..." }) if self.watching_thread thr = self.watching_thread.join(60) @mailbox.logger.info({ context: @mailbox.context, action: "Timeout waiting for watching thread" }) unless thr end @mailbox.logger.info({ context: @mailbox.context, action: "Done with thread cleanup" }) end private def connection @connection ||= ::MailRoom::IMAP::Connection.new(@mailbox) end end end gitlab-mail-room-0.0.9/lib/mail_room/logger/0000755000175100017510000000000014036627661017720 5ustar pravipravigitlab-mail-room-0.0.9/lib/mail_room/logger/structured.rb0000644000175100017510000000072114036627661022451 0ustar pravipravirequire 'logger' require 'json' module MailRoom module Logger class Structured < ::Logger def format_message(severity, timestamp, progname, message) raise ArgumentError.new("Message must be a Hash") unless message.is_a? Hash data = {} data[:severity] = severity data[:time] = timestamp || Time.now.to_s # only accept a Hash data.merge!(message) data.to_json + "\n" end end end end gitlab-mail-room-0.0.9/lib/mail_room/version.rb0000644000175100017510000000012414036627661020450 0ustar pravipravimodule MailRoom # Current version of gitlab-mail_room gem VERSION = "0.0.9" end gitlab-mail-room-0.0.9/lib/mail_room/imap/0000755000175100017510000000000014036627661017367 5ustar pravipravigitlab-mail-room-0.0.9/lib/mail_room/imap/message.rb0000644000175100017510000000050314036627661021336 0ustar pravipravi# frozen_string_literal:true module MailRoom module IMAP class Message < MailRoom::Message attr_reader :seqno def initialize(uid:, body:, seqno:) super(uid: uid, body: body) @seqno = seqno end def ==(other) super && seqno == other.seqno end end end end gitlab-mail-room-0.0.9/lib/mail_room/imap/connection.rb0000644000175100017510000001303314036627661022053 0ustar pravipravi# frozen_string_literal: true module MailRoom module IMAP class Connection < MailRoom::Connection def initialize(mailbox) super # log in and set the mailbox reset setup end # is the connection logged in? # @return [Boolean] def logged_in? @logged_in end # is the connection blocked idling? # @return [Boolean] def idling? @idling end # is the imap connection closed? # @return [Boolean] def disconnected? imap.disconnected? end # is the connection ready to idle? # @return [Boolean] def ready_to_idle? logged_in? && !idling? end def quit stop_idling reset end def wait # in case we missed any between idles process_mailbox idle process_mailbox rescue Net::IMAP::Error, IOError => e @mailbox.logger.warn({ context: @mailbox.context, action: 'Disconnected. Resetting...', error: e.message }) reset setup end private def reset @imap = nil @logged_in = false @idling = false end def setup @mailbox.logger.info({ context: @mailbox.context, action: 'Starting TLS session' }) start_tls @mailbox.logger.info({ context: @mailbox.context, action: 'Logging into mailbox' }) log_in @mailbox.logger.info({ context: @mailbox.context, action: 'Setting mailbox' }) set_mailbox end # build a net/imap connection to google imap def imap @imap ||= Net::IMAP.new(@mailbox.host, port: @mailbox.port, ssl: @mailbox.ssl_options) end # start a TLS session def start_tls imap.starttls if @mailbox.start_tls end # send the imap login command to google def log_in imap.login(@mailbox.email, @mailbox.password) @logged_in = true end # select the mailbox name we want to use def set_mailbox imap.select(@mailbox.name) if logged_in? end # is the response for a new message? # @param response [Net::IMAP::TaggedResponse] the imap response from idle # @return [Boolean] def message_exists?(response) response.respond_to?(:name) && response.name == 'EXISTS' end # @private def idle_handler ->(response) { imap.idle_done if message_exists?(response) } end # maintain an imap idle connection def idle return unless ready_to_idle? @mailbox.logger.info({ context: @mailbox.context, action: 'Idling' }) @idling = true imap.idle(@mailbox.idle_timeout, &idle_handler) ensure @idling = false end # trigger the idle to finish and wait for the thread to finish def stop_idling return unless idling? imap.idle_done # idling_thread.join # self.idling_thread = nil end def process_mailbox return unless @new_message_handler @mailbox.logger.info({ context: @mailbox.context, action: 'Processing started' }) msgs = new_messages any_deletions = msgs. # deliver each new message, collect success map(&@new_message_handler). # include messages with success zip(msgs). # filter failed deliveries, collect message select(&:first).map(&:last). # scrub delivered messages map { |message| scrub(message) } .any? imap.expunge if @mailbox.expunge_deleted && any_deletions end def scrub(message) if @mailbox.delete_after_delivery imap.store(message.seqno, '+FLAGS', [Net::IMAP::DELETED]) true end end # @private # fetch all messages for the new message ids def new_messages # Both of these calls may results in # imap raising an EOFError, we handle # this exception in the watcher messages_for_ids(new_message_ids) end # TODO: label messages? # @imap.store(id, "+X-GM-LABELS", [label]) # @private # search for all new (unseen) message ids # @return [Array] message ids def new_message_ids # uid_search still leaves messages UNSEEN all_unread = imap.uid_search(@mailbox.search_command) all_unread = all_unread.slice(0, @mailbox.limit_max_unread) if @mailbox.limit_max_unread.to_i > 0 to_deliver = all_unread.select { |uid| @mailbox.deliver?(uid) } @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: to_deliver } }) to_deliver end # @private # fetch the email for all given ids in RFC822 format # @param ids [Array] list of message ids # @return [Array] the net/imap messages for the given ids def messages_for_ids(uids) return [] if uids.empty? # uid_fetch marks as SEEN, will not be re-fetched for UNSEEN imap_messages = imap.uid_fetch(uids, 'RFC822') imap_messages.each_with_object([]) do |msg, messages| messages << ::MailRoom::IMAP::Message.new(uid: msg.attr['UID'], body: msg.attr['RFC822'], seqno: msg.seqno) end end end end end gitlab-mail-room-0.0.9/lib/mail_room/cli.rb0000644000175100017510000000345314036627661017542 0ustar pravipravimodule MailRoom # The CLI parses ARGV into configuration to start the coordinator with. # @author Tony Pitale class CLI attr_accessor :configuration, :coordinator, :options # Initialize a new CLI instance to handle option parsing from arguments # into configuration to start the coordinator running on all mailboxes # # @param args [Array] `ARGV` passed from `bin/mail_room` def initialize(args) @options = {} OptionParser.new do |parser| parser.banner = [ "Usage: #{@name} [-c config_file]\n", " #{@name} --help\n" ].compact.join parser.on('-c', '--config FILE') do |path| options[:config_path] = path end parser.on('-q', '--quiet') do options[:quiet] = true end parser.on('--log-exit-as') do |format| # accepts 'json' and 'plain' # 'plain' is equivalent to no format given options[:exit_error_format] = format unless format.nil? end # parser.on("-l", "--log FILE") do |path| # options[:log_path] = path # end parser.on_tail("-?", "--help", "Display this usage information.") do puts "#{parser}\n" exit end end.parse!(args) self.configuration = Configuration.new(options) self.coordinator = Coordinator.new(configuration.mailboxes, configuration.health_check) end # Start the coordinator running, sets up signal traps def start Signal.trap(:INT) do coordinator.running = false end Signal.trap(:TERM) do exit end coordinator.run rescue Exception => e # not just Errors, but includes lower-level Exceptions CrashHandler.new.handle(e, @options[:exit_error_format]) exit end end end gitlab-mail-room-0.0.9/lib/mail_room/crash_handler.rb0000644000175100017510000000066714036627661021574 0ustar pravipravi module MailRoom class CrashHandler SUPPORTED_FORMATS = %w[json none] def initialize(stream=STDOUT) @stream = stream end def handle(error, format) if format == 'json' @stream.puts json(error) return end raise error end private def json(error) { time: Time.now, severity: :fatal, message: error.message, backtrace: error.backtrace }.to_json end end end gitlab-mail-room-0.0.9/lib/mail_room/connection.rb0000644000175100017510000000056014036627661021126 0ustar pravipravi# frozen_string_literal: true module MailRoom class Connection attr_reader :mailbox, :new_message_handler def initialize(mailbox) @mailbox = mailbox end def on_new_message(&block) @new_message_handler = block end def wait raise NotImplementedError end def quit raise NotImplementedError end end end gitlab-mail-room-0.0.9/lib/mail_room/health_check.rb0000644000175100017510000000217314036627661021373 0ustar pravipravi# frozen_string_literal: true module MailRoom class HealthCheck attr_reader :address, :port, :running def initialize(attributes = {}) @address = attributes[:address] @port = attributes[:port] validate! end def run @server = create_server @thread = Thread.new do @server.start end @thread.abort_on_exception = true @running = true end def quit @running = false @server&.shutdown @thread&.join(60) end private def validate! raise 'No health check address specified' unless address raise "Health check port #{@port.to_i} is invalid" unless port.to_i.positive? end def create_server require 'webrick' server = ::WEBrick::HTTPServer.new(Port: port, BindAddress: address, AccessLog: []) server.mount_proc '/liveness' do |_req, res| handle_liveness(res) end server end def handle_liveness(res) if @running res.status = 200 res.body = "OK\n" else res.status = 500 res.body = "Not running\n" end end end end gitlab-mail-room-0.0.9/lib/mail_room.rb0000644000175100017510000000064514036627661016773 0ustar pravipravirequire 'net/imap' require 'optparse' require 'yaml' module MailRoom end require "mail_room/version" require "mail_room/configuration" require "mail_room/health_check" require "mail_room/mailbox" require "mail_room/mailbox_watcher" require "mail_room/message" require "mail_room/connection" require "mail_room/coordinator" require "mail_room/cli" require 'mail_room/logger/structured' require 'mail_room/crash_handler' gitlab-mail-room-0.0.9/README.md0000644000175100017510000003511414036627661015200 0ustar pravipravi# mail_room # ## Fork notice `mail_room` contains some merged functionality that GitLab requires, so this mirror fork is to help us release custom functionality. It needs to be more or less kept up to date with the original, so please feel free to incorporate changes to the upstream repo if you see them. ### Rationale This fork is required to reduce dependency on the upstream releases. The [original JSON structured logging PR](https://github.com/tpitale/mail_room/pull/88) was [released](https://github.com/tpitale/mail_room/commit/deb8fe63bab21c5c3003346961a815d137ff6d2d) and we [bumped the version](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/3719) to incorporate it into [omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab). It turned out that when Mailroom crashed out (which it's designed to do), the crash log [wasn't being pulled into elastic in a very useful way](https://github.com/tpitale/mail_room/commits/master) (that is, every line of the stack trace was a new event) so [another PR](https://github.com/tpitale/mail_room/pull/103) was raised. Rather than wait for the author (or bugging him more than once), we [opted for bias for action](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19186#note_290758986) and made a fork of the gem. Here it is [in omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/3960). The fork is useful as we can post quick fixes to our own fork and release fixes quickly, and still contribute those fixes upstream to help others. ## README mail_room is a configuration based process that will idle on IMAP connections and execute a delivery method when a new message is received. Examples of delivery methods include: * POST to a delivery URL (Postback) * Queue a job to Sidekiq or Que for later processing (Sidekiq or Que) * Log the message or open with LetterOpener (Logger or LetterOpener) [![Build Status](https://travis-ci.org/tpitale/mail_room.png?branch=master)](https://travis-ci.org/tpitale/mail_room) [![Code Climate](https://codeclimate.com/github/tpitale/mail_room/badges/gpa.svg)](https://codeclimate.com/github/tpitale/mail_room) ## Installation ## Add this line to your application's Gemfile: gem 'mail_room' And then execute: $ bundle Or install it yourself as: $ gem install mail_room You will also need to install `faraday` or `letter_opener` if you use the `postback` or `letter_opener` delivery methods, respectively. ## Usage ## mail_room -c /path/to/config.yml **Note:** To ignore missing config file or missing `mailboxes` key, use `-q` or `--quiet` ## Configuration ## ```yaml --- :health_check: - :address: "127.0.0.1" :port: 8080 :mailboxes: - :email: "user1@gmail.com" :password: "password" :name: "inbox" :search_command: 'NEW' :logger: :log_path: /path/to/logfile/for/mailroom :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" :content_type: "text/plain" - :email: "user2@gmail.com" :password: "password" :name: "inbox" :delivery_method: postback :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" - :email: "user3@gmail.com" :password: "password" :name: "inbox" :delivery_method: logger :delivery_options: :log_path: "/var/log/user3-email.log" - :email: "user4@gmail.com" :password: "password" :name: "inbox" :delivery_method: letter_opener :delete_after_delivery: true :expunge_deleted: true :delivery_options: :location: "/tmp/user4-email" - :email: "user5@gmail.com" :password: "password" :name: "inbox" :delivery_method: sidekiq :delivery_options: :redis_url: redis://localhost:6379 :worker: EmailReceiverWorker - :email: "user6@gmail.com" :password: "password" :name: "inbox" :delivery_method: sidekiq :delivery_options: # When pointing to sentinel, follow this sintax for redis URLs: # redis://:@/ :redis_url: redis://:password@my-redis-sentinel/ :sentinels: - :host: 127.0.0.1 :port: 26379 :worker: EmailReceiverWorker ``` **Note:** If using `delete_after_delivery`, you also probably want to use `expunge_deleted` unless you really know what you're doing. ## health_check ## Requires `webrick` gem to be installed. This option enables an HTTP server that listens to a bind address defined by `address` and `port`. The following endpoints are supported: * `/liveness`: This returns a 200 status code with `OK` as the body if the server is running. Otherwise, it returns a 500 status code. This feature is not included in upstream `mail_room` and is specific to GitLab. ## delivery_method ## ### postback ### Requires `faraday` gem be installed. *NOTE:* If you're using Ruby `>= 2.0`, you'll need to use Faraday from `>= 0.8.9`. Versions before this seem to have some weird behavior with `mail_room`. The default delivery method, requires `delivery_url` and `delivery_token` in configuration. You can pass `content_type:` option to overwrite `faraday's` default content-type(`application/x-www-form-urlencoded`) for post requests, we recommend passing `text/plain` as content-type. As the postback is essentially using your app as if it were an API endpoint, you may need to disable forgery protection as you would with a JSON API. ### sidekiq ### Deliver the message by pushing it onto the configured Sidekiq queue to be handled by a custom worker. Requires `redis` gem to be installed. Configured with `:delivery_method: sidekiq`. Delivery options: - **redis_url**: The Redis server to connect with. Use the same Redis URL that's used to configure Sidekiq. Required, defaults to `redis://localhost:6379`. - **sentinels**: A list of sentinels servers used to provide HA to Redis. (see [Sentinel Support](#sentinel-support)) Optional. - **namespace**: The Redis namespace Sidekiq works under. Use the same Redis namespace that's used to configure Sidekiq. Optional. - **queue**: The Sidekiq queue the job is pushed onto. Make sure Sidekiq actually reads off this queue. Required, defaults to `default`. - **worker**: The worker class that will handle the message. Required. An example worker implementation looks like this: ```ruby class EmailReceiverWorker include Sidekiq::Worker def perform(message) mail = Mail::Message.new(message) puts "New mail from #{mail.from.first}: #{mail.subject}" end end ``` ### que ### Deliver the message by pushing it onto the configured Que queue to be handled by a custom worker. Requires `pg` gem to be installed. Configured with `:delivery_method: que`. Delivery options: - **host**: The postgresql server host to connect with. Use the database you use with Que. Required, defaults to `localhost`. - **port**: The postgresql server port to connect with. Use the database you use with Que. Required, defaults to `5432`. - **database**: The postgresql database to use. Use the database you use with Que. Required. - **queue**: The Que queue the job is pushed onto. Make sure Que actually reads off this queue. Required, defaults to `default`. - **job_class**: The worker class that will handle the message. Required. - **priority**: The priority you want this job to run at. Required, defaults to `100`, lowest Que default priority. An example worker implementation looks like this: ```ruby class EmailReceiverJob < Que::Job def run(message) mail = Mail::Message.new(message) puts "New mail from #{mail.from.first}: #{mail.subject}" end end ``` ### logger ### Configured with `:delivery_method: logger`. If the `:log_path:` delivery option is not provided, defaults to `STDOUT` ### noop ### Configured with `:delivery_method: noop`. Does nothing, like it says. ### letter_opener ### Requires `letter_opener` gem be installed. Configured with `:delivery_method: letter_opener`. Uses Ryan Bates' excellent [letter_opener](https://github.com/ryanb/letter_opener) gem. ## ActionMailbox in Rails ## MailRoom can deliver mail to Rails using the ActionMailbox [configuration options for an SMTP relay](https://edgeguides.rubyonrails.org/action_mailbox_basics.html#configuration). In summary (from the ActionMailbox docs) 1. Configure Rails to use the `:relay` ingress option: ```rb # config/environments/production.rb config.action_mailbox.ingress = :relay ``` 2. Generate a strong password (e.g., using SecureRandom or something) and add it to Rails config: using `rails credentials:edit` under `action_mailbox.ingress_password`. And finally, configure MailRoom to use the postback configuration with the options: ```yaml :delivery_method: postback :delivery_options: :delivery_url: https://example.com/rails/action_mailbox/relay/inbound_emails :delivery_username: actionmailbox :delivery_password: ``` ## Receiving `postback` in Rails ## If you have a controller that you're sending to, with forgery protection disabled, you can get the raw string of the email using `request.body.read`. I would recommend having the `mail` gem bundled and parse the email using `Mail.read_from_string(request.body.read)`. *Note:* If you get the exception (`Rack::QueryParser::InvalidParameterError (invalid %-encoding...`) it's probably because the content-type is set to Faraday's default, which is `HEADERS['content-type'] = 'application/x-www-form-urlencoded'`. It can cause `Rack` to crash due to `InvalidParameterError` exception. When you send a post with `application/x-www-form-urlencoded`, `Rack` will attempt to parse the input and can end up raising an exception, for example if the email that you are forwarding contain `%%` in its content or headers it will cause Rack to crash with the message above. ## idle_timeout ## By default, the IDLE command will wait for 29 minutes (in order to keep the server connection happy). If you'd prefer not to wait that long, you can pass `imap_timeout` in seconds for your mailbox configuration. ## Search Command ## This setting allows configuration of the IMAP search command sent to the server. This still defaults 'UNSEEN'. You may find that 'NEW' works better for you. ## IMAP Server Configuration ## You can set per-mailbox configuration for the IMAP server's `host` (default: 'imap.gmail.com'), `port` (default: 993), `ssl` (default: true), and `start_tls` (default: false). If you want to set additional options for IMAP SSL you can pass a YAML hash to match [SSLContext#set_params](http://docs.ruby-lang.org/en/2.2.0/OpenSSL/SSL/SSLContext.html#method-i-set_params). If you set `verify_mode` to `:none` it'll replace with the appropriate constant. If you're seeing the error `Please log in via your web browser: https://support.google.com/mail/accounts/answer/78754 (Failure)`, you need to configure your Gmail account to allow less secure apps to access it: https://support.google.com/accounts/answer/6010255. ## Running in Production ## I suggest running with either upstart or init.d. Check out this wiki page for some example scripts for both: https://github.com/tpitale/mail_room/wiki/Init-Scripts-for-Running-mail_room ## Arbitration ## When running multiple instances of MailRoom against a single mailbox, to try to prevent delivery of the same message multiple times, we can configure Arbitration using Redis. ```yaml :mailboxes: - :email: "user1@gmail.com" :password: "password" :name: "inbox" :delivery_method: postback :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" :arbitration_method: redis :arbitration_options: # The Redis server to connect with. Defaults to redis://localhost:6379. :redis_url: redis://redis.example.com:6379 # The Redis namespace to house the Redis keys under. Optional. :namespace: mail_room - :email: "user2@gmail.com" :password: "password" :name: "inbox" :delivery_method: postback :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" :arbitration_method: redis :arbitration_options: # When pointing to sentinel, follow this sintax for redis URLs: # redis://:@/ :redis_url: redis://:password@my-redis-sentinel/ :sentinels: - :host: 127.0.0.1 :port: 26379 # The Redis namespace to house the Redis keys under. Optional. :namespace: mail_room ``` **Note:** This will likely never be a _perfect_ system for preventing multiple deliveries of the same message, so I would advise checking the unique `message_id` if you are running in this situation. **Note:** There are other scenarios for preventing duplication of messages at scale that _may_ be more appropriate in your particular setup. One such example is using multiple inboxes in reply-by-email situations. Another is to use labels and configure a different `SEARCH` command for each instance of MailRoom. ## Sentinel Support Redis Sentinel provides high availability for Redis. Please read their [documentation](http://redis.io/topics/sentinel) first, before enabling it with mail_room. To connect to a Sentinel, you need to setup authentication to both sentinels and redis daemons first, and make sure both are binding to a reachable IP address. In mail_room, when you are connecting to a Sentinel, you have to inform the `master-name` and the `password` through `redis_url` param, following this syntax: ``` redis://:@/ ``` You also have to inform at least one pair of `host` and `port` for a sentinel in your cluster. To have a minimum reliable setup, you need at least `3` sentinel nodes and `3` redis servers (1 master, 2 slaves). ## Logging ## MailRoom will output JSON-formatted logs to give some observability into its operations. Simply configure a `log_path` for the `logger` on any of your mailboxes. By default, nothing will be logged. If you wish to log to `STDOUT` or `STDERR` instead of a file, you can pass `:stdout` or `:stderr`, respectively and MailRoom will log there. ## Contributing ## 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request 6. If accepted, ask for commit rights ## TODO ## 1. specs, this is just a (working) proof of concept √ 2. finish code for POSTing to callback with auth √ 3. accept mailbox configuration for one account directly on the commandline; or ask for it 4. add example rails endpoint, with auth examples 5. add example configs for upstart/init.d √ 6. log to stdout √ 7. add a development mode that opens in letter_opener by ryanb √ gitlab-mail-room-0.0.9/.travis.yml0000644000175100017510000000017414036627661016030 0ustar pravipravilanguage: ruby rvm: - 2.3.0 - 2.4 - 2.5 - 2.6 services: - redis-server script: bundle exec rspec spec sudo: false gitlab-mail-room-0.0.9/.gitlab-ci.yml0000644000175100017510000000155314036627661016355 0ustar pravipravi# Cache gems in between builds services: - redis:latest .test-template: &test cache: paths: - vendor/ruby variables: REDIS_URL: redis://redis:6379 script: - bundle exec rspec spec before_script: - apt update && apt install -y libicu-dev - ruby -v # Print out ruby version for debugging # Uncomment next line if your rails app needs a JS runtime: # - apt-get update -q && apt-get install nodejs -yqq - gem install bundler --no-document # Bundler is not installed with the image - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby rspec-2.4: image: "ruby:2.4" <<: *test rspec-2.5: image: "ruby:2.5" <<: *test rspec-2.6: image: "ruby:2.6" <<: *test rspec-2.7: image: "ruby:2.7" <<: *test rspec-3.0: image: "ruby:3.0" <<: *test gitlab-mail-room-0.0.9/logfile.log0000644000175100017510000000010214036627661016032 0ustar pravipravi# Logfile created on 2019-09-26 08:59:35 -0500 by logger.rb/66358 gitlab-mail-room-0.0.9/.gitlab/0000755000175100017510000000000014036627661015235 5ustar pravipravigitlab-mail-room-0.0.9/.gitlab/issue_templates/0000755000175100017510000000000014036627661020443 5ustar pravipravigitlab-mail-room-0.0.9/.gitlab/issue_templates/Release.md0000644000175100017510000000135414036627661022350 0ustar pravipravi# GitLab mail_room release checklist - [ ] create tag in https://gitlab.com/gitlab-org/gitlab-mail_room/ - [ ] publish gem from this tag to rubygems.org - [ ] update https://gitlab.com/gitlab-org/gitlab/-/blob/master/Gemfile to use the new gem version - [ ] 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) - [ ] 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) gitlab-mail-room-0.0.9/Gemfile0000644000175100017510000000013614036627661015210 0ustar pravipravisource 'https://rubygems.org' # Specify your gem's dependencies in mail_room.gemspec gemspec gitlab-mail-room-0.0.9/CODE_OF_CONDUCT.md0000644000175100017510000000445414036627661016523 0ustar pravipravi# Contributor Code of Conduct As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery * Personal attacks * Trolling or insulting/derogatory comments * Public or private harassment * Publishing other's private information, such as physical or electronic addresses, without explicit permission * Other unethical or unprofessional conduct Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at [INSERT EMAIL ADDRESS]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/](http://contributor-covenant.org/version/1/3/0/) gitlab-mail-room-0.0.9/LICENSE.txt0000644000175100017510000000205314036627661015540 0ustar pravipraviCopyright (c) 2013 Tony Pitale MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.