mail_room 0.0.1 → 0.0.2

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