mail_room-0.6.1/0000755000004100000410000000000012612725214013526 5ustar www-datawww-datamail_room-0.6.1/Rakefile0000644000004100000410000000016512612725213015174 0ustar www-datawww-datarequire "bundler/gem_tasks" require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) task :default => :spec mail_room-0.6.1/bin/0000755000004100000410000000000012612725213014275 5ustar www-datawww-datamail_room-0.6.1/bin/mail_room0000755000004100000410000000011012612725213016171 0ustar www-datawww-data#!/usr/bin/env ruby require 'mail_room' MailRoom::CLI.new(ARGV).start mail_room-0.6.1/Gemfile0000644000004100000410000000013612612725213015020 0ustar www-datawww-datasource 'https://rubygems.org' # Specify your gem's dependencies in mail_room.gemspec gemspec mail_room-0.6.1/.ruby-version0000644000004100000410000000000512612725213016165 0ustar www-datawww-data2.2.2mail_room-0.6.1/CODE_OF_CONDUCT.md0000644000004100000410000000367512612725213016337 0ustar www-datawww-data# 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. 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 opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) mail_room-0.6.1/LICENSE.txt0000644000004100000410000000205312612725213015350 0ustar www-datawww-dataCopyright (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.mail_room-0.6.1/spec/0000755000004100000410000000000012612725214014460 5ustar www-datawww-datamail_room-0.6.1/spec/spec_helper.rb0000644000004100000410000000120712612725214017276 0ustar www-datawww-datarequire 'simplecov' SimpleCov.start require 'bundler/setup' require 'rspec' require 'mocha/api' require 'bourne' require 'fakeredis/rspec' require File.expand_path('../../lib/mail_room', __FILE__) RSpec.configure do |config| config.mock_with :mocha config.treat_symbols_as_metadata_keys_with_true_values = true 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 mail_room-0.6.1/spec/lib/0000755000004100000410000000000012612725214015226 5ustar www-datawww-datamail_room-0.6.1/spec/lib/cli_spec.rb0000644000004100000410000000230712612725214017336 0ustar www-datawww-datarequire '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)} describe '.new' do let(:args) {["-c", "a path"]} before :each do MailRoom::Configuration.stubs(:new).returns(configuration) MailRoom::Coordinator.stubs(:new).returns(coordinator) end it 'parses arguments into configuration' do MailRoom::CLI.new(args).configuration.should eq(configuration) MailRoom::Configuration.should have_received(:new).with({:config_path => 'a path'}) end it 'creates a new coordinator with configuration' do MailRoom::CLI.new(args).coordinator.should eq(coordinator) MailRoom::Coordinator.should have_received(:new).with(configuration.mailboxes) end end describe '#start' do let(:cli) {MailRoom::CLI.new([])} before :each do cli.configuration = configuration cli.coordinator = coordinator end it 'starts running the coordinator' do cli.start coordinator.should have_received(:run) end end end mail_room-0.6.1/spec/lib/mailbox_spec.rb0000644000004100000410000000556112612725214020227 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::Mailbox do describe "#deliver" do context "with arbitration_method of noop" do it 'arbitrates with a Noop instance' do mailbox = MailRoom::Mailbox.new({:arbitration_method => 'noop'}) noop = stub(:deliver?) MailRoom::Arbitration['noop'].stubs(:new => noop) uid = 123 mailbox.deliver?(uid) noop.should have_received(:deliver?).with(uid) end end context "with arbitration_method of redis" do it 'arbitrates with a Redis instance' do mailbox = MailRoom::Mailbox.new({:arbitration_method => 'redis'}) redis = stub(:deliver?) MailRoom::Arbitration['redis'].stubs(:new => redis) uid = 123 mailbox.deliver?(uid) redis.should have_received(:deliver?).with(uid) end end context "with delivery_method of noop" do it 'delivers with a Noop instance' do mailbox = MailRoom::Mailbox.new({:delivery_method => 'noop'}) noop = stub(:deliver) MailRoom::Delivery['noop'].stubs(:new => noop) mailbox.deliver(stub(:attr => {'RFC822' => 'a message'})) noop.should have_received(:deliver).with('a message') end end context "with delivery_method of logger" do it 'delivers with a Logger instance' do mailbox = MailRoom::Mailbox.new({:delivery_method => 'logger'}) logger = stub(:deliver) MailRoom::Delivery['logger'].stubs(:new => logger) mailbox.deliver(stub(:attr => {'RFC822' => 'a message'})) logger.should have_received(:deliver).with('a message') end end context "with delivery_method of postback" do it 'delivers with a Postback instance' do mailbox = MailRoom::Mailbox.new({:delivery_method => 'postback'}) postback = stub(:deliver) MailRoom::Delivery['postback'].stubs(:new => postback) mailbox.deliver(stub(:attr => {'RFC822' => 'a message'})) postback.should have_received(:deliver).with('a message') end end context "with delivery_method of letter_opener" do it 'delivers with a LetterOpener instance' do mailbox = MailRoom::Mailbox.new({:delivery_method => 'letter_opener'}) letter_opener = stub(:deliver) MailRoom::Delivery['letter_opener'].stubs(:new => letter_opener) mailbox.deliver(stub(:attr => {'RFC822' => 'a message'})) letter_opener.should have_received(:deliver).with('a message') end end context "without an RFC822 attribute" do it "doesn't deliver the message" do mailbox = MailRoom::Mailbox.new({:delivery_method => 'noop'}) noop = stub(:deliver) MailRoom::Delivery['noop'].stubs(:new => noop) mailbox.deliver(stub(:attr => {'FLAGS' => [:Seen, :Recent]})) noop.should have_received(:deliver).never end end end end mail_room-0.6.1/spec/lib/mailbox_watcher_spec.rb0000644000004100000410000001346512612725214021746 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::MailboxWatcher do let(:mailbox) {MailRoom::Mailbox.new} describe '#running?' do it 'is false by default' do watcher = MailRoom::MailboxWatcher.new(mailbox) watcher.running?.should eq(false) end end describe '#logged_in?' do it 'is false by default' do watcher = MailRoom::MailboxWatcher.new(mailbox) watcher.logged_in?.should eq(false) end end describe '#idling?' do it 'is false by default' do watcher = MailRoom::MailboxWatcher.new(mailbox) watcher.idling?.should eq(false) end end describe '#imap' do let(:mailbox) {MailRoom::Mailbox.new} it 'builds a new Net::IMAP object' do Net::IMAP.stubs(:new).returns('imap') MailRoom::MailboxWatcher.new(mailbox).imap.should eq('imap') Net::IMAP.should have_received(:new).with('imap.gmail.com', :port => 993, :ssl => true) end end describe '#setup' do let(:imap) {stub(:login => true, :select => true)} let(:mailbox) { MailRoom::Mailbox.new(:email => 'user1@gmail.com', :password => 'password', :name => 'inbox', ) } let(:watcher) { MailRoom::MailboxWatcher.new(mailbox) } it 'logs in and sets the mailbox to watch' do watcher.stubs(:imap).returns(imap) watcher.setup imap.should have_received(:login).with('user1@gmail.com', 'password') watcher.logged_in?.should eq(true) imap.should have_received(:select).with('inbox') end context 'with start_tls configured as true' do before(:each) do mailbox.start_tls = true imap.stubs(:starttls) watcher.stubs(:imap).returns(imap) end it 'sets up tls session on imap setup' do watcher.setup imap.should have_received(:starttls) end end end describe '#idle' do let(:imap) {stub} let(:watcher) {MailRoom::MailboxWatcher.new(nil)} before :each do watcher.stubs(:imap).returns(imap) end it 'returns if not logged in' do watcher.stubs(:logged_in?).returns(false) watcher.idle imap.should have_received(:idle).never end context "when logged in" do before :each do imap.stubs(:idle_done) watcher.stubs(:logged_in?).returns(true) end it 'handles any response with a name of EXISTS and stops idling' do response = stub(:name => 'EXISTS') imap.stubs(:idle).yields(response) watcher.idle imap.should have_received(:idle) imap.should have_received(:idle_done) end it 'does not finish idling when response is not EXISTS' do response = stub(:name => 'DESTROY') imap.stubs(:idle).yields(response) watcher.idle imap.should have_received(:idle) imap.should have_received(:idle_done).never end end end describe 'process_mailbox' do let(:imap) {stub} let(:mailbox) {MailRoom::Mailbox.new} let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)} it 'builds a new mailbox handler if none exists' do MailRoom::MailboxHandler.stubs(:new).returns(stub(:process)) watcher.stubs(:imap).returns(imap) watcher.process_mailbox MailRoom::MailboxHandler.should have_received(:new).with(mailbox, imap) end it 'processes with the handler' do handler = stub(:process) watcher.stubs(:handler).returns(handler) watcher.process_mailbox handler.should have_received(:process) end end describe '#stop_idling' do let(:imap) {stub} let(:idling_thread) {stub(:abort_on_exception=)} let(:watcher) {MailRoom::MailboxWatcher.new(nil)} before :each do watcher.stubs(:imap).returns(imap) watcher.stubs(:idling_thread).returns(idling_thread) end it "returns unless imap is idling" do imap.stubs(:idle_done) idling_thread.stubs(:join) watcher.stubs(:idling?).returns(false) watcher.stop_idling imap.should have_received(:idle_done).never idling_thread.should have_received(:join).never end context "when idling" do before :each do imap.stubs(:idle_done) idling_thread.stubs(:join) watcher.stubs(:idling?).returns(true) end it 'stops the idle' do watcher.stop_idling imap.should have_received(:idle_done) end it 'waits on the idling_thread to finish' do watcher.stop_idling idling_thread.should have_received(:join) end end end describe '#run' do let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)} before :each do Net::IMAP.stubs(:new).returns(stub) Thread.stubs(:start).yields.returns(stub(:abort_on_exception=)) watcher.stubs(:setup) watcher.handler.stubs(:process) end it 'sets up' do watcher.stubs(:running?).returns(false) watcher.run watcher.should have_received(:setup) end it 'starts a thread for idling' do watcher.stubs(:running?).returns(false) watcher.run Thread.should have_received(:start) end it 'loops while running' do watcher.stubs(:running?).returns(true, false) watcher.stubs(:idle) watcher.run watcher.should have_received(:running?).times(2) end it 'idles' do watcher.stubs(:running?).returns(true, false) watcher.stubs(:idle) watcher.run watcher.should have_received(:idle).once end it 'processes messages' do watcher.stubs(:running?).returns(true, false) watcher.stubs(:idle) watcher.run watcher.handler.should have_received(:process).times(2) end end describe '#quit' do let(:watcher) {MailRoom::MailboxWatcher.new(mailbox)} it 'stops idling' do watcher.stubs(:stop_idling) watcher.quit watcher.should have_received(:stop_idling) end end end mail_room-0.6.1/spec/lib/arbitration/0000755000004100000410000000000012612725214017544 5ustar www-datawww-datamail_room-0.6.1/spec/lib/arbitration/redis_spec.rb0000644000004100000410000000260412612725214022213 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/arbitration/redis' describe MailRoom::Arbitration::Redis do let(:mailbox) { MailRoom::Mailbox.new( arbitration_options: { namespace: "mail_room" } ) } let(:options) { described_class::Options.new(mailbox) } subject { described_class.new(options) } # Private, but we don't care. let(:redis) { subject.send(:redis) } describe '#deliver?' do context "when called the first time" do 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 subject.deliver?(123) end it "returns false" do expect(subject.deliver?(123)).to be_falsey end it "increments the delivered flag" do subject.deliver?(123) expect(redis.get("delivered:123")).to eq("2") end end context "when called for another uid" do before do subject.deliver?(123) end it "returns true" do expect(subject.deliver?(234)).to be_truthy end end end end mail_room-0.6.1/spec/lib/configuration_spec.rb0000644000004100000410000000133412612725214021435 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::Configuration do let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))} describe 'set_mailboxes' do it 'parses yaml into mailbox objects' do MailRoom::Mailbox.stubs(:new).returns('mailbox1', 'mailbox2') configuration = MailRoom::Configuration.new(:config_path => config_path) configuration.mailboxes.should eq(['mailbox1', 'mailbox2']) end it 'sets mailboxes to an empty set when config_path is missing' do MailRoom::Mailbox.stubs(:new) configuration = MailRoom::Configuration.new configuration.mailboxes.should eq([]) MailRoom::Mailbox.should have_received(:new).never end end end mail_room-0.6.1/spec/lib/coordinator_spec.rb0000644000004100000410000000356712612725214021123 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::Coordinator do describe '#initialize' do it 'builds a watcher for each mailbox' do MailRoom::MailboxWatcher.stubs(:new).returns('watcher1', 'watcher2') coordinator = MailRoom::Coordinator.new(['mailbox1', 'mailbox2']) coordinator.watchers.should eq(['watcher1', 'watcher2']) MailRoom::MailboxWatcher.should have_received(:new).with('mailbox1') MailRoom::MailboxWatcher.should have_received(:new).with('mailbox2') end it 'makes no watchers when mailboxes is empty' do coordinator = MailRoom::Coordinator.new([]) coordinator.watchers.should eq([]) end end describe '#run' do it 'runs each watcher' do watcher = stub watcher.stubs(:run) watcher.stubs(:quit) MailRoom::MailboxWatcher.stubs(:new).returns(watcher) coordinator = MailRoom::Coordinator.new(['mailbox1']) coordinator.stubs(:sleep_while_running) coordinator.run watcher.should have_received(:run) watcher.should have_received(:quit) end it 'should go to sleep after running watchers' do coordinator = MailRoom::Coordinator.new([]) coordinator.stubs(:running=) coordinator.stubs(:running?).returns(false) coordinator.run coordinator.should have_received(:running=).with(true) coordinator.should have_received(:running?) end it 'should set attribute running to true' do coordinator = MailRoom::Coordinator.new([]) coordinator.stubs(:sleep_while_running) coordinator.run coordinator.running.should 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']) coordinator.quit watcher.should have_received(:quit) end end end mail_room-0.6.1/spec/lib/mailbox_handler_spec.rb0000644000004100000410000000376112612725214021724 0ustar www-datawww-datarequire 'spec_helper' describe MailRoom::MailboxHandler do describe 'process mailbox' do let(:imap) {stub} let(:mailbox) {MailRoom::Mailbox.new} it 'fetches and delivers all new messages from ids' do imap.stubs(:uid_search).returns([1,2]) message1 = stub(:attr => {'RFC822' => 'message1'}) message2 = stub(:attr => {'RFC822' => 'message2'}) imap.stubs(:uid_fetch).returns([message1, message2]) mailbox.stubs(:deliver) handler = MailRoom::MailboxHandler.new(mailbox, imap) handler.process imap.should have_received(:uid_search).with('UNSEEN') imap.should have_received(:uid_fetch).with([1,2], 'RFC822') mailbox.should have_received(:deliver).with(message1) mailbox.should have_received(:deliver).with(message2) end it "fetches and delivers all new messages from ids that haven't been processed yet" do imap.stubs(:uid_search).returns([1,2]) message1 = stub(:attr => {'RFC822' => 'message1'}) message2 = stub(:attr => {'RFC822' => 'message2'}) imap.stubs(:uid_fetch).returns([message2]) mailbox.stubs(:deliver) mailbox.stubs(:deliver?).with(1).returns(false) mailbox.stubs(:deliver?).with(2).returns(true) handler = MailRoom::MailboxHandler.new(mailbox, imap) handler.process imap.should have_received(:uid_search).with('UNSEEN') imap.should have_received(:uid_fetch).with([2], 'RFC822') mailbox.should have_received(:deliver).with(message1).never mailbox.should have_received(:deliver).with(message2) end it 'returns no messages if there are no ids' do imap.stubs(:uid_search).returns([]) imap.stubs(:uid_fetch) mailbox.search_command = 'NEW' mailbox.stubs(:deliver) handler = MailRoom::MailboxHandler.new(mailbox, imap) handler.process imap.should have_received(:uid_search).with('NEW') imap.should have_received(:uid_fetch).never mailbox.should have_received(:deliver).never end end end mail_room-0.6.1/spec/lib/delivery/0000755000004100000410000000000012612725214017051 5ustar www-datawww-datamail_room-0.6.1/spec/lib/delivery/que_spec.rb0000644000004100000410000000214312612725214021202 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/que' describe MailRoom::Delivery::Que do describe '#deliver' do let(:mailbox) {MailRoom::Mailbox.new({ 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.stubs(:connect).returns(connection) connection.stubs(:exec) MailRoom::Delivery::Que.new(options).deliver('email') expect(PG).to have_received(:connect).with({ host: 'localhost', port: 5432, dbname: 'delivery_test', user: 'postgres', password: '' }) expect(connection).to have_received(:exec).with( "INSERT INTO que_jobs (priority, job_class, queue, args) VALUES ($1, $2, $3, $4)", [ 5, 'ParseMailJob', 'default', JSON.dump(['email']) ] ) end end end mail_room-0.6.1/spec/lib/delivery/logger_spec.rb0000644000004100000410000000225212612725214021670 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/logger' describe MailRoom::Delivery::Logger do describe '#initialize' do context "without a log path" do let(:mailbox) {MailRoom::Mailbox.new} it 'creates a new ruby logger' do ::Logger.stubs(:new) MailRoom::Delivery::Logger.new(mailbox) ::Logger.should have_received(:new).with(STDOUT) end end context "with a log path" do let(:mailbox) {MailRoom::Mailbox.new(:log_path => '/var/log/mail-room.log')} it 'creates a new file to append to' do ::Logger.stubs(:new) file = stub(:sync=) ::File.stubs(:open).returns(file) MailRoom::Delivery::Logger.new(mailbox) File.should have_received(:open).with('/var/log/mail-room.log', 'a') ::Logger.should have_received(:new).with(file) end end end describe '#deliver' do let(:mailbox) {MailRoom::Mailbox.new} it 'writes the message to info' do logger = stub(:info) ::Logger.stubs(:new).returns(logger) MailRoom::Delivery::Logger.new(mailbox).deliver('a message') logger.should have_received(:info).with('a message') end end end mail_room-0.6.1/spec/lib/delivery/letter_opener_spec.rb0000644000004100000410000000162312612725214023261 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/letter_opener' describe MailRoom::Delivery::LetterOpener do describe '#deliver' do let(:mailbox) {MailRoom::Mailbox.new(: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) MailRoom::Delivery::LetterOpener.new(mailbox).deliver('a message') end it 'creates a new LetterOpener::DeliveryMethod' do ::LetterOpener::DeliveryMethod.should have_received(:new).with(:location => '/tmp/somewhere') end it 'parses the message string with Mail' do ::Mail.should have_received(:read_from_string).with('a message') end it 'delivers the mail message' do delivery_method.should have_received(:deliver!).with(mail) end end endmail_room-0.6.1/spec/lib/delivery/postback_spec.rb0000644000004100000410000000154612612725214022224 0ustar www-datawww-datarequire 'spec_helper' require 'mail_room/delivery/postback' describe MailRoom::Delivery::Postback do describe '#deliver' do let(:mailbox) {MailRoom::Mailbox.new({ :delivery_url => 'http://localhost/inbox', :delivery_token => 'abcdefg' })} it 'posts the message with faraday' do connection = stub request = stub Faraday.stubs(:new).returns(connection) connection.stubs(:token_auth) connection.stubs(:post).yields(request) request.stubs(:url) request.stubs(:body=) MailRoom::Delivery::Postback.new(mailbox).deliver('a message') connection.should have_received(:token_auth).with('abcdefg') connection.should have_received(:post) request.should have_received(:url).with('http://localhost/inbox') request.should have_received(:body=).with('a message') end end end mail_room-0.6.1/spec/fixtures/0000755000004100000410000000000012612725214016331 5ustar www-datawww-datamail_room-0.6.1/spec/fixtures/test_config.yml0000644000004100000410000000051612612725214021362 0ustar www-datawww-data--- :mailboxes: - :email: "user1@gmail.com" :password: "password" :name: "inbox" :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" - :email: "user2@gmail.com" :password: "password" :name: "inbox" :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" mail_room-0.6.1/.travis.yml0000644000004100000410000000014712612725213015640 0ustar www-datawww-datalanguage: ruby rvm: - 1.9.3 - 2.0.0 - 2.1.5 - 2.2.0 script: bundle exec rspec spec sudo: false mail_room-0.6.1/lib/0000755000004100000410000000000012612725213014273 5ustar www-datawww-datamail_room-0.6.1/lib/mail_room.rb0000644000004100000410000000044512612725213016601 0ustar www-datawww-datarequire 'net/imap' require 'optparse' require 'yaml' module MailRoom end require "mail_room/version" require "mail_room/configuration" require "mail_room/mailbox" require "mail_room/mailbox_watcher" require "mail_room/mailbox_handler" require "mail_room/coordinator" require "mail_room/cli" mail_room-0.6.1/lib/mail_room/0000755000004100000410000000000012612725214016252 5ustar www-datawww-datamail_room-0.6.1/lib/mail_room/configuration.rb0000644000004100000410000000161512612725213021450 0ustar www-datawww-datarequire "erb" module MailRoom # Wraps configuration for a set of individual mailboxes with global config # @author Tony Pitale class Configuration attr_accessor :mailboxes, :daemonize, :log_path, :pid_path, :quiet # Initialize a new configuration of mailboxes def initialize(options={}) self.mailboxes = [] self.quiet = options.fetch(:quiet, false) if options.has_key?(:config_path) begin config_file = YAML.load(ERB.new(File.read(options[:config_path])).result) set_mailboxes(config_file[:mailboxes]) 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 end end mail_room-0.6.1/lib/mail_room/mailbox_watcher.rb0000644000004100000410000000743512612725214021760 0ustar www-datawww-datamodule MailRoom # TODO: split up between processing and idling? # Watch a Mailbox # @author Tony Pitale class MailboxWatcher attr_accessor :idling_thread, :timeout_thread # Watch a new mailbox # @param mailbox [MailRoom::Mailbox] the mailbox to watch def initialize(mailbox) @mailbox = mailbox reset @running = false end # build a net/imap connection to google imap def imap @imap ||= Net::IMAP.new(@mailbox.host, :port => @mailbox.port, :ssl => @mailbox.ssl) end # build a handler to process mailbox messages def handler @handler ||= MailboxHandler.new(@mailbox, imap) end # are we running? # @return [Boolean] def running? @running 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 # 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 # log in and set the mailbox def setup reset start_tls log_in set_mailbox end # clear disconnected imap # reset imap state def reset @imap = nil @logged_in = false @idling = false 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 # maintain an imap idle connection def idle return unless ready_to_idle? @idling = true self.timeout_thread = Thread.start do # The IMAP server 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 sleep 29 * 60 imap.idle_done if idling? end timeout_thread.abort_on_exception = true imap.idle(&idle_handler) ensure timeout_thread.kill if timeout_thread self.timeout_thread = nil @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 # run the mailbox watcher def run setup @running = true # prefetch messages before first idle process_mailbox self.idling_thread = Thread.start do while(running?) do begin # block until we stop idling idle # when new messages are ready process_mailbox rescue Net::IMAP::Error, IOError => e # we've been disconnected, so re-setup setup end end end idling_thread.abort_on_exception = true end # stop running def quit @running = false stop_idling # disconnect end # trigger the handler to process this mailbox for new messages def process_mailbox handler.process end private # @private def idle_handler lambda {|response| imap.idle_done if message_exists?(response)} end end end mail_room-0.6.1/lib/mail_room/arbitration.rb0000644000004100000410000000037712612725213021123 0ustar www-datawww-datamodule MailRoom module Arbitration def [](name) require_relative("./arbitration/#{name}") case name when "redis" Arbitration::Redis else Arbitration::Noop end end module_function :[] end end mail_room-0.6.1/lib/mail_room/mailbox.rb0000644000004100000410000000412612612725214020235 0ustar www-datawww-datarequire "mail_room/delivery" require "mail_room/arbitration" module MailRoom # Mailbox Configuration fields MAILBOX_FIELDS = [ :email, :password, :host, :port, :ssl, :start_tls, :search_command, :name, :delete_after_delivery, :delivery_method, # :noop, :logger, :postback, :letter_opener :log_path, # for logger :delivery_url, # for postback :delivery_token, # for postback :location, # for letter_opener :delivery_options, :arbitration_method, :arbitration_options ] # 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 # Default attributes for the mailbox configuration DEFAULTS = { :search_command => 'UNSEEN', :delivery_method => 'postback', :host => 'imap.gmail.com', :port => 993, :ssl => true, :start_tls => false, :delete_after_delivery => false, :delivery_options => {}, :arbitration_method => 'noop', :arbitration_options => {} } # Store the configuration and require the appropriate delivery method # @param attributes [Hash] configuration options def initialize(attributes={}) super(*DEFAULTS.merge(attributes).values_at(*members)) end def 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) arbitrator.deliver?(uid) end # deliver the imap email message # @param message [Net::IMAP::FetchData] def deliver(message) body = message.attr['RFC822'] return true unless body delivery.deliver(body) end private def parsed_arbitration_options arbitration_klass::Options.new(self) end def parsed_delivery_options delivery_klass::Options.new(self) end end end mail_room-0.6.1/lib/mail_room/cli.rb0000644000004100000410000000261712612725213017353 0ustar www-datawww-datamodule MailRoom # The CLI parses ARGV into configuration to start the coordinator with. # @author Tony Pitale class CLI attr_accessor :configuration, :coordinator # 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("-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) 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 end end end mail_room-0.6.1/lib/mail_room/version.rb0000644000004100000410000000011412612725214020260 0ustar www-datawww-datamodule MailRoom # Current version of MailRoom gem VERSION = "0.6.1" end mail_room-0.6.1/lib/mail_room/arbitration/0000755000004100000410000000000012612725213020567 5ustar www-datawww-datamail_room-0.6.1/lib/mail_room/arbitration/redis.rb0000644000004100000410000000305712612725213022227 0ustar www-datawww-datarequire "redis" module MailRoom module Arbitration class Redis Options = Struct.new(:redis_url, :namespace) do def initialize(mailbox) redis_url = mailbox.arbitration_options[:redis_url] || "redis://localhost:6379" namespace = mailbox.arbitration_options[:namespace] super(redis_url, namespace) 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) key = "delivered:#{uid}" incr = nil redis.multi do |client| # At this point, `incr` is a future, which will get its value after # the MULTI command returns. incr = client.incr(key) client.expire(key, EXPIRATION) end # If INCR returns 1, that means the key didn't exist before, which means # we are the first mail_room to try to deliver this message, so we get to. # If we get any other value, another mail_room already (tried to) deliver # the message, so we don't have to anymore. incr.value == 1 end private def redis @redis ||= begin redis = ::Redis.new(url: options.redis_url) namespace = options.namespace if namespace require 'redis/namespace' ::Redis::Namespace.new(namespace, redis: redis) else redis end end end end end end mail_room-0.6.1/lib/mail_room/arbitration/noop.rb0000644000004100000410000000036612612725213022074 0ustar www-datawww-datamodule MailRoom module Arbitration class Noop Options = Class.new do def initialize(*) super() end end def initialize(*) end def deliver?(*) true end end end end mail_room-0.6.1/lib/mail_room/mailbox_handler.rb0000644000004100000410000000332112612725214021726 0ustar www-datawww-datamodule MailRoom # Fetches new email messages for delivery # @author Tony Pitale class MailboxHandler # build a handler for this mailbox and our imap connection # @param mailbox [MailRoom::Mailbox] the mailbox configuration # @param imap [Net::IMAP::Connection] the open connection to gmail def initialize(mailbox, imap) @mailbox = mailbox @imap = imap end # deliver each of the new messages def process # return if idling? || !running? new_messages.each do |message| # loop over delivery methods and deliver each delivered = @mailbox.deliver(message) if delivered && @mailbox.delete_after_delivery @imap.store(message.seqno, "+FLAGS", [Net::IMAP::DELETED]) end end @imap.expunge if @mailbox.delete_after_delivery end private # @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 @imap.uid_search(@mailbox.search_command).select { |uid| @mailbox.deliver?(uid) } 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? @imap.uid_fetch(uids, "RFC822") end end end mail_room-0.6.1/lib/mail_room/delivery.rb0000644000004100000410000000066612612725213020431 0ustar www-datawww-datamodule 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 mail_room-0.6.1/lib/mail_room/delivery/0000755000004100000410000000000012612725214020075 5ustar www-datawww-datamail_room-0.6.1/lib/mail_room/delivery/sidekiq.rb0000644000004100000410000000431012612725214022051 0ustar www-datawww-datarequire "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, :queue, :worker) do def initialize(mailbox) redis_url = mailbox.delivery_options[:redis_url] || "redis://localhost:6379" namespace = mailbox.delivery_options[:namespace] queue = mailbox.delivery_options[:queue] || "default" worker = mailbox.delivery_options[:worker] super(redis_url, namespace, queue, worker) 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)) true end private def client client = Redis.new(url: options.redis_url) namespace = options.namespace if namespace require 'redis/namespace' Redis::Namespace.new(namespace, redis: client) else client 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 mail_room-0.6.1/lib/mail_room/delivery/letter_opener.rb0000644000004100000410000000164312612725214023275 0ustar www-datawww-datarequire '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 mail_room-0.6.1/lib/mail_room/delivery/logger.rb0000644000004100000410000000165412612725214021707 0ustar www-datawww-datarequire '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 mail_room-0.6.1/lib/mail_room/delivery/postback.rb0000644000004100000410000000233312612725214022231 0ustar www-datawww-datarequire 'faraday' module MailRoom module Delivery # Postback Delivery method # @author Tony Pitale class Postback Options = Struct.new(:delivery_url, :delivery_token) do def initialize(mailbox) delivery_url = mailbox.delivery_url || mailbox.delivery_options[:delivery_url] delivery_token = mailbox.delivery_token || mailbox.delivery_options[:delivery_token] super(delivery_url, delivery_token) end end # Build a new delivery, hold the delivery options # @param [MailRoom::Delivery::Postback::Options] def initialize(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 connection.token_auth @delivery_options.delivery_token connection.post do |request| request.url @delivery_options.delivery_url request.body = message # request.options[:timeout] = 3 # request.headers['Content-Type'] = 'text/plain' end true end end end end mail_room-0.6.1/lib/mail_room/delivery/que.rb0000644000004100000410000000351312612725214021216 0ustar www-datawww-datarequire '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) 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] super(host, port, database, username, password, queue, priority, job_class) 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) 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 mail_room-0.6.1/lib/mail_room/delivery/noop.rb0000644000004100000410000000057012612725214021377 0ustar www-datawww-datamodule 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 mail_room-0.6.1/lib/mail_room/coordinator.rb0000644000004100000410000000146112612725213021123 0ustar www-datawww-datamodule MailRoom # Coordinate the mailbox watchers # @author Tony Pitale class Coordinator attr_accessor :watchers, :running # build watchers for a set of mailboxes # @params mailboxes [Array] mailboxes to be watched def initialize(mailboxes) self.watchers = [] mailboxes.each {|box| self.watchers << MailboxWatcher.new(box)} end alias :running? :running # start each of the watchers to running def 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 watchers.each(&:quit) end private # @private def sleep_while_running while(running?) do; sleep 1; end end end end mail_room-0.6.1/metadata.yml0000644000004100000410000001541512612725214016037 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: mail_room version: !ruby/object:Gem::Version version: 0.6.1 platform: ruby authors: - Tony Pitale autorequire: bindir: bin cert_chain: [] date: 2015-10-12 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: mocha requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: bourne requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: simplecov requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: fakeredis requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: faraday requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: mail requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: letter_opener requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: redis-namespace requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: pg requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: charlock_holmes requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' description: mail_room will proxy email (gmail) from IMAP to a delivery method email: - tpitale@gmail.com executables: - mail_room extensions: [] extra_rdoc_files: [] files: - ".gitignore" - ".ruby-version" - ".travis.yml" - CHANGELOG.md - CODE_OF_CONDUCT.md - Gemfile - LICENSE.txt - README.md - Rakefile - bin/mail_room - lib/mail_room.rb - lib/mail_room/arbitration.rb - lib/mail_room/arbitration/noop.rb - lib/mail_room/arbitration/redis.rb - lib/mail_room/cli.rb - lib/mail_room/configuration.rb - lib/mail_room/coordinator.rb - lib/mail_room/delivery.rb - lib/mail_room/delivery/letter_opener.rb - lib/mail_room/delivery/logger.rb - lib/mail_room/delivery/noop.rb - lib/mail_room/delivery/postback.rb - lib/mail_room/delivery/que.rb - lib/mail_room/delivery/sidekiq.rb - lib/mail_room/mailbox.rb - lib/mail_room/mailbox_handler.rb - lib/mail_room/mailbox_watcher.rb - lib/mail_room/version.rb - mail_room.gemspec - spec/fixtures/test_config.yml - spec/lib/arbitration/redis_spec.rb - spec/lib/cli_spec.rb - spec/lib/configuration_spec.rb - spec/lib/coordinator_spec.rb - spec/lib/delivery/letter_opener_spec.rb - spec/lib/delivery/logger_spec.rb - spec/lib/delivery/postback_spec.rb - spec/lib/delivery/que_spec.rb - spec/lib/mailbox_handler_spec.rb - spec/lib/mailbox_spec.rb - spec/lib/mailbox_watcher_spec.rb - spec/spec_helper.rb homepage: http://github.com/tpitale/mail_room licenses: [] metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.4.5 signing_key: specification_version: 4 summary: mail_room will proxy email (gmail) from IMAP to a callback URL, logger, or letter_opener test_files: - spec/fixtures/test_config.yml - spec/lib/arbitration/redis_spec.rb - spec/lib/cli_spec.rb - spec/lib/configuration_spec.rb - spec/lib/coordinator_spec.rb - spec/lib/delivery/letter_opener_spec.rb - spec/lib/delivery/logger_spec.rb - spec/lib/delivery/postback_spec.rb - spec/lib/delivery/que_spec.rb - spec/lib/mailbox_handler_spec.rb - spec/lib/mailbox_spec.rb - spec/lib/mailbox_watcher_spec.rb - spec/spec_helper.rb mail_room-0.6.1/.gitignore0000644000004100000410000000023512612725213015515 0ustar www-datawww-data*.gem *.rbc .bundle .config .yardoc Gemfile.lock InstalledFiles _yardoc coverage doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp logmail_room-0.6.1/mail_room.gemspec0000644000004100000410000000256112612725214017055 0ustar www-datawww-data# -*- 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 = "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" gem.add_development_dependency "mocha" gem.add_development_dependency "bourne" gem.add_development_dependency "simplecov" gem.add_development_dependency "fakeredis" # 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-namespace" gem.add_development_dependency "pg" gem.add_development_dependency "charlock_holmes" end mail_room-0.6.1/CHANGELOG.md0000644000004100000410000000276612612725213015351 0ustar www-datawww-data## 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>* mail_room-0.6.1/README.md0000644000004100000410000002016112612725213015004 0ustar www-datawww-data# mail_room # 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.png)](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 --- :mailboxes: - :email: "user1@gmail.com" :password: "password" :name: "inbox" :search_command: 'NEW' :delivery_options: :delivery_url: "http://localhost:3000/inbox" :delivery_token: "abcdefg" - :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 :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 ``` ## 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. 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. In our case, the postback is plaintext, but the protection will still need to be disabled. ### 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`. - **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 `:log_path:` 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. ## 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)`. ## 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'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 ``` **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. ## 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 √