mail_room 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format nested
data/README.md CHANGED
@@ -18,28 +18,47 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage ##
20
20
 
21
- bin/mail_room -f /path/to/config.yml -d
21
+ bin/mail_room -c /path/to/config.yml
22
22
 
23
23
  ## Configuration ##
24
24
 
25
25
  ---
26
- :mailboxes:
27
- -
28
- :email: "user1@gmail.com"
29
- :password: "password"
30
- :name: "inbox"
31
- :delivery_url: "http://localhost:3000/inbox"
32
- :delivery_token: "abcdefg"
33
- -
34
- :email: "user2@gmail.com"
35
- :password: "password"
36
- :name: "inbox"
37
- :delivery_url: "http://localhost:3000/inbox"
38
- :delivery_token: "abcdefg"
39
-
40
- ## Dependencies ##
41
-
42
- * celluloid
26
+ :mailboxes:
27
+ -
28
+ :email: "user1@gmail.com"
29
+ :password: "password"
30
+ :name: "inbox"
31
+ :delivery_url: "http://localhost:3000/inbox"
32
+ :delivery_token: "abcdefg"
33
+ -
34
+ :email: "user2@gmail.com"
35
+ :password: "password"
36
+ :name: "inbox"
37
+ :delivery_method: postback
38
+ :delivery_url: "http://localhost:3000/inbox"
39
+ :delivery_token: "abcdefg"
40
+ -
41
+ :email: "user3@gmail.com"
42
+ :password: "password"
43
+ :name: "inbox"
44
+ :delivery_method: logger
45
+ :log_path: "/var/log/user3-email.log"
46
+ -
47
+ :email: "user4@gmail.com"
48
+ :password: "password"
49
+ :name: "inbox"
50
+ :delivery_method: letter_opener
51
+ :location: "/tmp/user4-email"
52
+
53
+ ## delivery_method ##
54
+
55
+ ### postback ###
56
+
57
+ ### logger ###
58
+
59
+ ### noop ###
60
+
61
+ ### letter_opener ###
43
62
 
44
63
  ## Contributing ##
45
64
 
@@ -49,3 +68,13 @@ Or install it yourself as:
49
68
  4. Push to the branch (`git push origin my-new-feature`)
50
69
  5. Create new Pull Request
51
70
  6. If accepted, ask for commit rights
71
+
72
+ ## TODO ##
73
+
74
+ 1. specs, this is just a (working) proof of concept
75
+ 2. finish code for POSTing to callback with auth √
76
+ 3. accept mailbox configuration for one account directly on the commandline; or ask for it
77
+ 4. add example rails endpoint, with auth examples
78
+ 5. add example configs for god/upstart
79
+ 6. log to stdout √
80
+ 7. add a development mode that opens in letter_opener by ryanb √
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/mail_room CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  require 'mail_room'
4
4
 
5
- MailRoom::CLI.new(ARGV).run
5
+ MailRoom::CLI.new(ARGV).start
data/lib/mail_room/cli.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module MailRoom
2
2
  class CLI
3
- attr_accessor :configuration
3
+ attr_accessor :configuration, :coordinator
4
4
 
5
5
  def initialize(args)
6
6
  options = {}
@@ -15,17 +15,9 @@ module MailRoom
15
15
  options[:config_path] = path
16
16
  end
17
17
 
18
- parser.on("-d", "--daemon", "Daemonize mode") do |v|
19
- options[:daemonize] = v
20
- end
21
-
22
- parser.on("-l", "--log FILE") do |path|
23
- options[:log_path] = path
24
- end
25
-
26
- parser.on("-p", "--pid FILE") do |path|
27
- options[:pid_path] = path
28
- end
18
+ # parser.on("-l", "--log FILE") do |path|
19
+ # options[:log_path] = path
20
+ # end
29
21
 
30
22
  parser.on_tail("-?", "--help", "Display this usage information.") do
31
23
  puts "#{parser}\n"
@@ -34,39 +26,10 @@ module MailRoom
34
26
  end.parse!(args)
35
27
 
36
28
  self.configuration = Configuration.new(options)
37
- end
38
-
39
- def run
40
- if configuration.daemonize?
41
- daemonize
42
- else
43
- start
44
- end
45
- end
46
-
47
- def running?
48
- @running
49
- end
50
-
51
- def daemonize
52
- exit if fork
53
- Process.setsid
54
- exit if fork
55
- store_pid(Process.pid)
56
- File.umask 0000
57
- redirect_output!
58
-
59
- start
60
-
61
- clean_pid
29
+ self.coordinator = Coordinator.new(configuration.mailboxes)
62
30
  end
63
31
 
64
32
  def start
65
- @running = true
66
-
67
- @coordinator ||= Coordinator.new(configuration.mailboxes)
68
- @coordinator.run
69
-
70
33
  Signal.trap(:INT) do
71
34
  stop
72
35
  end
@@ -75,56 +38,11 @@ module MailRoom
75
38
  exit
76
39
  end
77
40
 
78
- while(running?) do; sleep 1; end
41
+ coordinator.run
79
42
  end
80
43
 
81
44
  def stop
82
- return unless @coordinator
83
-
84
- @coordinator.quit
85
-
86
- @running = false
87
- end
88
-
89
- def store_pid(pid)
90
- pid_path = configuration.pid_path
91
-
92
- puts pid_path
93
-
94
- FileUtils.mkdir_p(File.dirname(pid_path))
95
- File.open(pid_path, 'w'){|f| f.write("#{pid}\n")}
96
- end
97
-
98
- def clean_pid
99
- pid_path = configuration.pid_path
100
-
101
- begin
102
- FileUtils.rm pid_path if File.exists?(pid_path)
103
- rescue => e
104
- end
105
- end
106
-
107
-
108
- def redirect_output!
109
- if log_path = configuration.log_path
110
- # if the log directory doesn't exist, create it
111
- FileUtils.mkdir_p File.dirname(log_path), :mode => 0755
112
- # touch the log file to create it
113
- FileUtils.touch log_path
114
- # Set permissions on the log file
115
- File.chmod(0644, log_path)
116
- # Reopen $stdout (NOT +STDOUT+) to start writing to the log file
117
- $stdout.reopen(log_path, 'a')
118
- # Redirect $stderr to $stdout
119
- $stderr.reopen $stdout
120
- $stdout.sync = true
121
- else # redirect to /dev/null
122
- # We're not bothering to sync if we're dumping to /dev/null
123
- # because /dev/null doesn't care about buffered output
124
- $stdin.reopen '/dev/null'
125
- $stdout.reopen '/dev/null', 'a'
126
- $stderr.reopen $stdout
127
- end
45
+ coordinator.quit
128
46
  end
129
47
  end
130
48
  end
@@ -3,22 +3,19 @@ module MailRoom
3
3
  attr_accessor :mailboxes, :daemonize, :log_path, :pid_path
4
4
 
5
5
  def initialize(options={})
6
- config_file = YAML.load_file(options[:config_path]) if options.has_key?(:config_path)
7
-
8
6
  self.mailboxes = []
9
- load_mailboxes(config_file[:mailboxes])
10
7
 
11
- self.daemonize = options.fetch(:daemonize, false)
12
- self.log_path = options.fetch(:log_path, nil)
13
- self.pid_path = options.fetch(:pid_path, "/var/run/mail_room.pid")
8
+ if options.has_key?(:config_path)
9
+ config_file = YAML.load_file(options[:config_path])
10
+
11
+ set_mailboxes(config_file[:mailboxes])
12
+ end
14
13
  end
15
14
 
16
- def load_mailboxes(mailboxes_config)
15
+ def set_mailboxes(mailboxes_config)
17
16
  mailboxes_config.each do |attributes|
18
17
  self.mailboxes << Mailbox.new(attributes)
19
18
  end
20
19
  end
21
-
22
- alias :daemonize? :daemonize
23
20
  end
24
21
  end
@@ -1,19 +1,27 @@
1
1
  module MailRoom
2
2
  class Coordinator
3
- attr_accessor :handlers
3
+ attr_accessor :watchers, :running
4
4
 
5
5
  def initialize(mailboxes)
6
- self.handlers = []
6
+ self.watchers = []
7
7
 
8
- mailboxes.each {|mb| self.handlers << MessageHandler.new(mb)}
8
+ mailboxes.each {|box| self.watchers << MailboxWatcher.new(box)}
9
9
  end
10
10
 
11
+ alias :running? :running
12
+
11
13
  def run
12
- handlers.each(&:run!)
14
+ watchers.each(&:run)
15
+
16
+ self.running = true
17
+
18
+ while(running?) do; sleep 1; end
13
19
  end
14
20
 
15
21
  def quit
16
- handlers.each(&:quit!)
22
+ watchers.each(&:quit)
23
+
24
+ self.running = false
17
25
  end
18
26
  end
19
27
  end
@@ -0,0 +1,17 @@
1
+ require 'mail'
2
+ require 'letter_opener'
3
+
4
+ module MailRoom
5
+ module Delivery
6
+ class LetterOpener
7
+ def initialize(mailbox)
8
+ @mailbox = mailbox
9
+ end
10
+
11
+ def deliver(message)
12
+ method = ::LetterOpener::DeliveryMethod.new(:location => @mailbox.location)
13
+ method.deliver!(Mail.read_from_string(message))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ require 'logger'
2
+
3
+ module MailRoom
4
+ module Delivery
5
+ class Logger
6
+ def initialize(mailbox)
7
+ io = File.open(mailbox.log_path, 'a') if mailbox.log_path
8
+ io ||= STDOUT
9
+
10
+ io.sync = true
11
+
12
+ @logger = ::Logger.new(io)
13
+ end
14
+
15
+ def deliver(message)
16
+ @logger.info message
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module MailRoom
2
+ module Delivery
3
+ class Noop
4
+ def initialize(*)
5
+ end
6
+
7
+ def deliver(*)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ require 'faraday'
2
+
3
+ module MailRoom
4
+ module Delivery
5
+ class Postback
6
+ def initialize(mailbox)
7
+ @mailbox = mailbox
8
+ end
9
+
10
+ def deliver(message)
11
+ connection = Faraday.new
12
+ connection.token_auth @mailbox.delivery_token
13
+
14
+ connection.post do |request|
15
+ request.url @mailbox.delivery_url
16
+ request.body = message
17
+ # request.options[:timeout] = 3
18
+ # request.headers['Content-Type'] = 'text/plain'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,9 +1,38 @@
1
1
  module MailRoom
2
- Mailbox = Struct.new(:email, :password, :name, :delivery_url, :delivery_token)
2
+ Mailbox = Struct.new(*[
3
+ :email,
4
+ :password,
5
+ :name,
6
+ :delivery_method, # :noop, :logger, :postback, :letter_opener
7
+ :log_path, # for logger
8
+ :delivery_url, # for postback
9
+ :delivery_token, # for postback
10
+ :location # for letter_opener
11
+ ])
3
12
 
4
13
  class Mailbox
5
14
  def initialize(attributes={})
6
15
  super(*attributes.values_at(*members))
16
+
17
+ require_relative("./delivery/#{(delivery_method || 'postback')}")
18
+ end
19
+
20
+ # move to a mailbox deliverer class?
21
+ def delivery_klass
22
+ case delivery_method
23
+ when "noop"
24
+ Delivery::Noop
25
+ when "logger"
26
+ Delivery::Logger
27
+ when "letter_opener"
28
+ Delivery::LetterOpener
29
+ else
30
+ Delivery::Postback
31
+ end
32
+ end
33
+
34
+ def deliver(message)
35
+ delivery_klass.new(self).deliver(message.attr['RFC822'])
7
36
  end
8
37
  end
9
38
  end
@@ -0,0 +1,36 @@
1
+ module MailRoom
2
+ class MailboxHandler
3
+ def initialize(mailbox, imap)
4
+ @mailbox = mailbox
5
+ @imap = imap
6
+ end
7
+
8
+ def process
9
+ # return if idling? || !running?
10
+
11
+ new_messages.each do |msg|
12
+ # puts msg.attr['RFC822']
13
+
14
+ # loop over delivery methods and deliver each
15
+ @mailbox.deliver(msg)
16
+ end
17
+ end
18
+
19
+ def new_messages
20
+ messages_for_ids(new_message_ids)
21
+ end
22
+
23
+ # label messages?
24
+ # @imap.store(id, "+X-GM-LABELS", [label])
25
+
26
+ def new_message_ids
27
+ @imap.search('UNSEEN')
28
+ end
29
+
30
+ def messages_for_ids(ids)
31
+ return [] if ids.empty?
32
+
33
+ @imap.fetch(ids, "RFC822")
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,93 @@
1
+ module MailRoom
2
+ # split up between processing and idling?
3
+ class MailboxWatcher
4
+ attr_accessor :idling_thread
5
+
6
+ def initialize(mailbox)
7
+ @mailbox = mailbox
8
+
9
+ @running = false
10
+ @logged_in = false
11
+ @idling = false
12
+ end
13
+
14
+ def imap
15
+ @imap ||= Net::IMAP.new('imap.gmail.com', :port => 993, :ssl => true)
16
+ end
17
+
18
+ def handler
19
+ @handler ||= MailboxHandler.new(@mailbox, imap)
20
+ end
21
+
22
+ def running?
23
+ @running
24
+ end
25
+
26
+ def logged_in?
27
+ @logged_in
28
+ end
29
+
30
+ def idling?
31
+ @idling
32
+ end
33
+
34
+ def setup
35
+ log_in
36
+ set_mailbox
37
+ end
38
+
39
+ def log_in
40
+ imap.login(@mailbox.email, @mailbox.password)
41
+ @logged_in = true
42
+ end
43
+
44
+ def set_mailbox
45
+ imap.select(@mailbox.name) if logged_in?
46
+ end
47
+
48
+ def idle
49
+ return unless logged_in?
50
+
51
+ @idling = true
52
+
53
+ imap.idle do |response|
54
+ if response.respond_to?(:name) && response.name == 'EXISTS'
55
+ imap.idle_done
56
+ end
57
+ end
58
+
59
+ @idling = false
60
+ end
61
+
62
+ def process_mailbox
63
+ handler.process
64
+ end
65
+
66
+ def stop_idling
67
+ return unless idling?
68
+
69
+ imap.idle_done
70
+ idling_thread.join
71
+ end
72
+
73
+ def run
74
+ setup
75
+
76
+ @running = true
77
+
78
+ self.idling_thread = Thread.start do
79
+ while(running?) do
80
+ # block until we stop idling
81
+ idle
82
+ # when new messages are ready
83
+ process_mailbox
84
+ end
85
+ end
86
+ end
87
+
88
+ def quit
89
+ @running = false
90
+ stop_idling
91
+ end
92
+ end
93
+ end
@@ -1,3 +1,3 @@
1
1
  module MailRoom
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/mail_room.rb CHANGED
@@ -1,15 +1,17 @@
1
- require 'celluloid'
2
1
  require 'net/imap'
3
- require 'fileutils'
4
2
  require 'optparse'
5
3
  require 'yaml'
6
4
 
7
5
  module MailRoom
6
+ # def self.logger
7
+ # @logger ||= Logger.new(STDOUT)
8
+ # end
8
9
  end
9
10
 
10
11
  require "mail_room/version"
11
12
  require "mail_room/configuration"
12
13
  require "mail_room/mailbox"
13
- require "mail_room/message_handler"
14
+ require "mail_room/mailbox_watcher"
15
+ require "mail_room/mailbox_handler"
14
16
  require "mail_room/coordinator"
15
17
  require "mail_room/cli"
data/mail_room.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |gem|
8
8
  gem.version = MailRoom::VERSION
9
9
  gem.authors = ["Tony Pitale"]
10
10
  gem.email = ["tpitale@gmail.com"]
11
- gem.description = %q{mail_room will proxy email (gmail) from IMAP to a callback URL}
12
- gem.summary = %q{mail_room will proxy email (gmail) from IMAP to a callback URL}
11
+ gem.description = %q{mail_room will proxy email (gmail) from IMAP to a delivery method}
12
+ gem.summary = %q{mail_room will proxy email (gmail) from IMAP to a callback URL, logger, or letter_opener}
13
13
  gem.homepage = "http://github.com/tpitale/mail_room"
14
14
 
15
15
  gem.files = `git ls-files`.split($/)
@@ -17,5 +17,14 @@ Gem::Specification.new do |gem|
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
19
 
20
- gem.add_dependency 'celluloid'
20
+ gem.add_development_dependency "rake"
21
+ gem.add_development_dependency "rspec"
22
+ gem.add_development_dependency "mocha"
23
+ gem.add_development_dependency "bourne"
24
+ gem.add_development_dependency "simplecov"
25
+
26
+ # for testing delivery methods
27
+ gem.add_development_dependency "faraday"
28
+ gem.add_development_dependency "mail"
29
+ gem.add_development_dependency "letter_opener"
21
30
  end
@@ -0,0 +1,14 @@
1
+ ---
2
+ :mailboxes:
3
+ -
4
+ :email: "user1@gmail.com"
5
+ :password: "password"
6
+ :name: "inbox"
7
+ :delivery_url: "http://localhost:3000/inbox"
8
+ :delivery_token: "abcdefg"
9
+ -
10
+ :email: "user2@gmail.com"
11
+ :password: "password"
12
+ :name: "inbox"
13
+ :delivery_url: "http://localhost:3000/inbox"
14
+ :delivery_token: "abcdefg"
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe MailRoom::CLI do
4
+ let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))}
5
+ let!(:configuration) {MailRoom::Configuration.new({:config_path => config_path})}
6
+ let(:coordinator) {stub(:run => true, :quit => true)}
7
+
8
+ describe '.new' do
9
+ let(:args) {["-c", "a path"]}
10
+
11
+ before :each do
12
+ MailRoom::Configuration.stubs(:new).returns(configuration)
13
+ MailRoom::Coordinator.stubs(:new).returns(coordinator)
14
+ end
15
+
16
+ it 'parses arguments into configuration' do
17
+ MailRoom::CLI.new(args).configuration.should eq(configuration)
18
+ MailRoom::Configuration.should have_received(:new).with({:config_path => 'a path'})
19
+ end
20
+
21
+ it 'creates a new coordinator with configuration' do
22
+ MailRoom::CLI.new(args).coordinator.should eq(coordinator)
23
+ MailRoom::Coordinator.should have_received(:new).with(configuration.mailboxes)
24
+ end
25
+ end
26
+
27
+ describe '#start' do
28
+ let(:cli) {MailRoom::CLI.new([])}
29
+
30
+ before :each do
31
+ cli.configuration = configuration
32
+ cli.coordinator = coordinator
33
+ end
34
+
35
+ it 'starts running the coordinator' do
36
+ cli.start
37
+
38
+ coordinator.should have_received(:run)
39
+ end
40
+ end
41
+
42
+ describe '#stop' do
43
+ let(:cli) {MailRoom::CLI.new([])}
44
+
45
+ before :each do
46
+ cli.configuration = configuration
47
+ cli.coordinator = coordinator
48
+ end
49
+
50
+ it 'quits the coordinator' do
51
+ cli.stop
52
+
53
+ coordinator.should have_received(:quit)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe MailRoom::Configuration do
4
+ let(:config_path) {File.expand_path('../fixtures/test_config.yml', File.dirname(__FILE__))}
5
+
6
+ describe 'set_mailboxes' do
7
+ it 'parses yaml into mailbox objects' do
8
+ MailRoom::Mailbox.stubs(:new).returns('mailbox1', 'mailbox2')
9
+
10
+ configuration = MailRoom::Configuration.new(:config_path => config_path)
11
+
12
+ configuration.mailboxes.should eq(['mailbox1', 'mailbox2'])
13
+ end
14
+
15
+ it 'sets mailboxes to an empty set when config_path is missing' do
16
+ MailRoom::Mailbox.stubs(:new)
17
+
18
+ configuration = MailRoom::Configuration.new
19
+
20
+ configuration.mailboxes.should eq([])
21
+
22
+ MailRoom::Mailbox.should have_received(:new).never
23
+ end
24
+ end
25
+ end