mail_daemon 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +51 -0
- data/Rakefile +2 -0
- data/bin/mail_daemon +22 -0
- data/config.yml +27 -0
- data/lib/mail_daemon.rb +71 -0
- data/lib/mail_daemon/email_body_parser.rb +28 -0
- data/lib/mail_daemon/email_handler.rb +109 -0
- data/lib/mail_daemon/email_watcher.rb +155 -0
- data/lib/mail_daemon/encryption.rb +42 -0
- data/lib/mail_daemon/helpers.rb +16 -0
- data/lib/mail_daemon/imap/connection.rb +155 -0
- data/lib/mail_daemon/imap/statuses.rb +17 -0
- data/lib/mail_daemon/imap/watcher.rb +42 -0
- data/lib/mail_daemon/imap_watcher.rb +80 -0
- data/lib/mail_daemon/version.rb +3 -0
- data/lib/public/custom.css +19 -0
- data/lib/public/dashboard.css +105 -0
- data/lib/server.rb +6 -0
- data/lib/views/settings.erb +113 -0
- data/lib/views/stats.erb +100 -0
- data/mail_daemon.gemspec +35 -0
- data/test/lib/order2cb/order_test.rb +20 -0
- data/test/lib/order2cb/version_test.rb +7 -0
- data/test/lib/order2cb_test.rb +25 -0
- data/test/test_helper.rb +3 -0
- metadata +221 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module MailDaemon
|
4
|
+
class Encryption
|
5
|
+
|
6
|
+
def initialize()
|
7
|
+
@key = ENV["CASEBLOCKS_ENCRYPTION_KEY"] || "YLX0IBT+OXaO4mP2bVYqzMPbrrss8eUcX1XtgLxlVH8="
|
8
|
+
@iv = ENV["CASEBLOCKS_ENCRYPTION_IV"] || "vvSVfoWvZQ3T/DfjsjO/9w=="
|
9
|
+
end
|
10
|
+
|
11
|
+
def encrypt(data)
|
12
|
+
unless data.nil?
|
13
|
+
cipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
14
|
+
cipher.encrypt
|
15
|
+
cipher.key = Base64.decode64(@key)
|
16
|
+
cipher.iv = Base64.decode64(@iv)
|
17
|
+
encrypted = cipher.update(data) + cipher.final
|
18
|
+
|
19
|
+
# [0..-2] strip off trailing carriage return
|
20
|
+
Base64.encode64(encrypted)[0..-2]
|
21
|
+
else
|
22
|
+
data
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def decrypt(value)
|
27
|
+
unless value.nil?
|
28
|
+
decipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
29
|
+
decipher.decrypt
|
30
|
+
decipher.key = Base64.decode64(@key)
|
31
|
+
decipher.iv = Base64.decode64(@iv)
|
32
|
+
|
33
|
+
encrypted = Base64.decode64(value)
|
34
|
+
decipher.update(encrypted) + decipher.final
|
35
|
+
else
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module MailDaemon
|
2
|
+
module Helpers
|
3
|
+
def setup_options(options)
|
4
|
+
@options = options.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
5
|
+
end
|
6
|
+
def default_option(name, value)
|
7
|
+
@options[name.to_s.to_sym] = value unless @options.has_key?(name.to_s.to_sym)
|
8
|
+
end
|
9
|
+
def required_option(names)
|
10
|
+
names_array = Array(names)
|
11
|
+
names_array.each do |name|
|
12
|
+
raise "#{name} is a required option, please supply this in the call to new" unless @options.has_key?(name.to_s.to_sym)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'net/imap'
|
2
|
+
|
3
|
+
module MailDaemon
|
4
|
+
module Imap
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
include MailDaemon::Helpers
|
8
|
+
include MailDaemon::Imap::Statuses
|
9
|
+
SECONDS_IN_MINUTES = 60
|
10
|
+
|
11
|
+
def initialize(options, &block)
|
12
|
+
setup_options(options)
|
13
|
+
|
14
|
+
required_option [:host, :username, :password]
|
15
|
+
|
16
|
+
default_option :port, 143
|
17
|
+
default_option :folder, "inbox"
|
18
|
+
default_option :ssl, false
|
19
|
+
default_option :start_tls, false
|
20
|
+
default_option :ssl_options, false
|
21
|
+
default_option :sleep_time, 10000
|
22
|
+
default_option :imap_timeout, 60
|
23
|
+
default_option :idle_recycle_time, 29 * SECONDS_IN_MINUTES
|
24
|
+
|
25
|
+
@notify_status_block = block
|
26
|
+
|
27
|
+
set_status(INITIALIZING)
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def login
|
32
|
+
set_status(CONNECTING)
|
33
|
+
@imap = Net::IMAP.new(@options[:host], :port => @options[:port], :ssl => @options[:ssl_options])
|
34
|
+
capabilities = @imap.capability
|
35
|
+
@idle_available = capabilities.include?("IDLE")
|
36
|
+
|
37
|
+
set_status(LOGGING_ON)
|
38
|
+
@imap.login(@options[:username], @options[:password])
|
39
|
+
|
40
|
+
# ap @imap.list("", "*")
|
41
|
+
|
42
|
+
@options[:folder] = "INBOX"
|
43
|
+
@imap.select(@options[:folder])
|
44
|
+
|
45
|
+
# puts "starting tls"
|
46
|
+
# @imap.starttls({}, verify=false) if @options[:start_tls]
|
47
|
+
|
48
|
+
@idle_required = true
|
49
|
+
|
50
|
+
set_status(LOGGED_ON)
|
51
|
+
end
|
52
|
+
|
53
|
+
def wait_for_messages(&block)
|
54
|
+
fetch_message_ids.each do |message_id|
|
55
|
+
yield fetch_message(message_id)
|
56
|
+
end
|
57
|
+
|
58
|
+
@watcher_block = block
|
59
|
+
|
60
|
+
if @idle_available
|
61
|
+
while(@idle_required)
|
62
|
+
|
63
|
+
recycle_thread = Thread.new(Time.now + @options[:idle_recycle_time]) do |end_time|
|
64
|
+
begin
|
65
|
+
while Time.now < end_time
|
66
|
+
sleep 1
|
67
|
+
end
|
68
|
+
@imap.idle_done
|
69
|
+
rescue => e
|
70
|
+
puts e.message
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
@idleing = true
|
75
|
+
@imap.idle do |message|
|
76
|
+
begin
|
77
|
+
if message.members.include?(:data) && !message[:data].kind_of?(Fixnum) && message[:data].members.include?(:text) && message[:data][:text] == "idling"
|
78
|
+
set_status(IDLEING)
|
79
|
+
else
|
80
|
+
@imap.idle_done
|
81
|
+
end
|
82
|
+
rescue => e
|
83
|
+
puts e.message
|
84
|
+
end
|
85
|
+
end
|
86
|
+
recycle_thread.kill
|
87
|
+
@idleing = false
|
88
|
+
fetch_message_ids.each do |message_id|
|
89
|
+
yield fetch_message(message_id)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
else
|
93
|
+
set_status(POLLING)
|
94
|
+
while(@idle_required)
|
95
|
+
fetch_message_ids.each do |message_id|
|
96
|
+
yield fetch_message(message_id)
|
97
|
+
end
|
98
|
+
sleep @options[:sleep_time]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def logout
|
104
|
+
@imap.idle_done if @idleing
|
105
|
+
set_status(LOGGING_OFF)
|
106
|
+
@imap.logout
|
107
|
+
set_status(LOGGED_OFF)
|
108
|
+
end
|
109
|
+
|
110
|
+
def disconnect
|
111
|
+
@idle_required = false
|
112
|
+
logout unless @imap.disconnected?
|
113
|
+
|
114
|
+
set_status(DISCONNECTING)
|
115
|
+
@imap.disconnect
|
116
|
+
set_status(DISCONNECTED)
|
117
|
+
end
|
118
|
+
|
119
|
+
def status
|
120
|
+
{:status => @status}
|
121
|
+
end
|
122
|
+
|
123
|
+
def running?
|
124
|
+
!@imap.disconnected?
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
def send_notification(message)
|
129
|
+
@notify_status_block.call :mailbox => @options, :type => "status_update", :status => message
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_status(status)
|
133
|
+
@status = status
|
134
|
+
send_notification status
|
135
|
+
end
|
136
|
+
|
137
|
+
def fetch_message_ids
|
138
|
+
messages = []
|
139
|
+
@options[:search_command] = "UNSEEN"
|
140
|
+
|
141
|
+
# ap @imap.status("inbox", [@options[:search_command]])
|
142
|
+
@imap.uid_search(@options[:search_command])
|
143
|
+
# begin
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
def fetch_message(id)
|
148
|
+
envelope = @imap.uid_fetch(id, ["RFC822"])
|
149
|
+
# TODO: maybe move this to after sent to queue
|
150
|
+
# @imap.store(message_id, "-FLAGS", [:Seen])
|
151
|
+
envelope[0][:attr]["RFC822"]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module MailDaemon
|
2
|
+
module Imap
|
3
|
+
module Statuses
|
4
|
+
INITIALIZING = "initializing"
|
5
|
+
CONNECTING = "connecting"
|
6
|
+
CONNECTED = "connected"
|
7
|
+
LOGGING_ON = "logging_on"
|
8
|
+
LOGGED_ON = "logged_on"
|
9
|
+
LOGGING_OFF = "logging_off"
|
10
|
+
LOGGED_OFF = "logged_off"
|
11
|
+
IDLEING = "ready"
|
12
|
+
POLLING = "ready"
|
13
|
+
DISCONNECTING = "disconnecting"
|
14
|
+
DISCONNECTED = "disconnected"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
module MailDaemon
|
3
|
+
module Imap
|
4
|
+
class Watcher
|
5
|
+
|
6
|
+
def initialize(options)
|
7
|
+
@options = options
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
def start(&block)
|
12
|
+
@connection = Imap::Connection.new(@options, &block)
|
13
|
+
@connection.login
|
14
|
+
# @thread = Thread.new do
|
15
|
+
@connection.wait_for_messages do |message|
|
16
|
+
yield :type => "incoming_email", :mailbox => @options, :inbound_message => message
|
17
|
+
end
|
18
|
+
# end
|
19
|
+
# @thread.join
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
@connection.disconnect
|
24
|
+
# @thread.terminate
|
25
|
+
end
|
26
|
+
|
27
|
+
def restart
|
28
|
+
stop
|
29
|
+
start
|
30
|
+
end
|
31
|
+
|
32
|
+
def running?
|
33
|
+
@connection.running?
|
34
|
+
end
|
35
|
+
|
36
|
+
def mailbox
|
37
|
+
@options[:mailbox]
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
module MailDaemon
|
3
|
+
class ImapWatcher
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
@watchers = []
|
8
|
+
|
9
|
+
raise "REDIS_URL environment variable is required (eg redis://localhost:6739)" unless ENV["REDIS_URL"]
|
10
|
+
raise "MYSQL_HOST environment variable is required" unless ENV["MYSQL_HOST"]
|
11
|
+
raise "MYSQL_DATABASE environment variable is required" unless ENV["MYSQL_DATABASE"]
|
12
|
+
raise "MYSQL_USERNAME environment variable is required" unless ENV["MYSQL_USERNAME"]
|
13
|
+
raise "MYSQL_PASSWORD environment variable is required" unless ENV["MYSQL_PASSWORD"]
|
14
|
+
|
15
|
+
ENV["MYSQL_PASSWORD"] = "" unless ENV["MYSQL_PASSWORD"]
|
16
|
+
ENV["MYSQL_PORT"] = "3306"
|
17
|
+
|
18
|
+
redis_url = URI.parse(ENV["REDIS_URL"])
|
19
|
+
|
20
|
+
$redis = Redis.new(:host => redis_url.host, :port => redis_url.port)
|
21
|
+
|
22
|
+
Signal.trap("INT") {
|
23
|
+
Thread.new {self.stop}.join
|
24
|
+
}
|
25
|
+
|
26
|
+
# Trap `Kill `
|
27
|
+
Signal.trap("TERM") {
|
28
|
+
Thread.new {self.stop}.join
|
29
|
+
}
|
30
|
+
|
31
|
+
restart
|
32
|
+
end
|
33
|
+
|
34
|
+
def setup_watchers
|
35
|
+
# load uptodate config
|
36
|
+
|
37
|
+
watchers = []
|
38
|
+
mysql_client do |mysql|
|
39
|
+
statement = mysql.prepare("SELECT * FROM case_blocks_email_accounts where imap_enabled=1")
|
40
|
+
result = statement.execute()
|
41
|
+
result.each do |row|
|
42
|
+
ssl = row["imap_ssl"]==1 ? {:verify_mode => "none"} : false
|
43
|
+
decrypted_password = Encryption.new.decrypt(row["imap_encrypted_password"])
|
44
|
+
watchers << Imap::Connection.new({:email => row["imap_username"], :host => row["imap_host"], :port => row["imap_port"], :password => decrypted_password, :ssl => ssl, :start_tls => row["imap_start_tls"]==1, :name => row["imap_folder_name"], :search_command => row["imap_search_command"], :message_count => row["imap_messages_processed"], :last_delivered_at => row["imap_last_delivered_at"], :delivery_method=>"sidekiq", :delivery_options=>{:redis_url => ENV["REDIS_URL"], :queue => "email_handler", :worker=>"EmailHandlerWorker"}})
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def start
|
51
|
+
watchers.each do |watcher|
|
52
|
+
watcher.watch do |message|
|
53
|
+
ap message
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def stop
|
59
|
+
watchers.map{|watcher| watcher.stop! }
|
60
|
+
end
|
61
|
+
|
62
|
+
def restart
|
63
|
+
stop
|
64
|
+
setup_watchers
|
65
|
+
start
|
66
|
+
end
|
67
|
+
|
68
|
+
def running?
|
69
|
+
!!@watchers.detect{|watcher| watcher.running? }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def mysql_client(&block)
|
74
|
+
client = Mysql2::Client.new(:host => ENV["MYSQL_HOST"], :port => ENV["MYSQL_PORT"], :username => ENV["MYSQL_USERNAME"], :password => ENV["MYSQL_PASSWORD"], :database => ENV["MYSQL_DATABASE"])
|
75
|
+
yield client
|
76
|
+
client.close
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
.status-circle {
|
2
|
+
width: 10px;
|
3
|
+
height: 10px;
|
4
|
+
background-color: #ccc;
|
5
|
+
border-radius: 50%;
|
6
|
+
margin-top: 6px;
|
7
|
+
}
|
8
|
+
|
9
|
+
.status-circle.red {
|
10
|
+
background-color: red;
|
11
|
+
}
|
12
|
+
|
13
|
+
.status-circle.green {
|
14
|
+
background-color: green;
|
15
|
+
}
|
16
|
+
|
17
|
+
.status-circle.orange {
|
18
|
+
background-color: orange;
|
19
|
+
}
|
@@ -0,0 +1,105 @@
|
|
1
|
+
/*
|
2
|
+
* Base structure
|
3
|
+
*/
|
4
|
+
|
5
|
+
/* Move down content because we have a fixed navbar that is 50px tall */
|
6
|
+
body {
|
7
|
+
padding-top: 50px;
|
8
|
+
}
|
9
|
+
|
10
|
+
|
11
|
+
/*
|
12
|
+
* Global add-ons
|
13
|
+
*/
|
14
|
+
|
15
|
+
.sub-header {
|
16
|
+
padding-bottom: 10px;
|
17
|
+
border-bottom: 1px solid #eee;
|
18
|
+
}
|
19
|
+
|
20
|
+
/*
|
21
|
+
* Top navigation
|
22
|
+
* Hide default border to remove 1px line.
|
23
|
+
*/
|
24
|
+
.navbar-fixed-top {
|
25
|
+
border: 0;
|
26
|
+
}
|
27
|
+
|
28
|
+
/*
|
29
|
+
* Sidebar
|
30
|
+
*/
|
31
|
+
|
32
|
+
/* Hide for mobile, show later */
|
33
|
+
.sidebar {
|
34
|
+
display: none;
|
35
|
+
}
|
36
|
+
@media (min-width: 768px) {
|
37
|
+
.sidebar {
|
38
|
+
position: fixed;
|
39
|
+
top: 51px;
|
40
|
+
bottom: 0;
|
41
|
+
left: 0;
|
42
|
+
z-index: 1000;
|
43
|
+
display: block;
|
44
|
+
padding: 20px;
|
45
|
+
overflow-x: hidden;
|
46
|
+
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
47
|
+
background-color: #f5f5f5;
|
48
|
+
border-right: 1px solid #eee;
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
/* Sidebar navigation */
|
53
|
+
.nav-sidebar {
|
54
|
+
margin-right: -21px; /* 20px padding + 1px border */
|
55
|
+
margin-bottom: 20px;
|
56
|
+
margin-left: -20px;
|
57
|
+
}
|
58
|
+
.nav-sidebar > li > a {
|
59
|
+
padding-right: 20px;
|
60
|
+
padding-left: 20px;
|
61
|
+
}
|
62
|
+
.nav-sidebar > .active > a,
|
63
|
+
.nav-sidebar > .active > a:hover,
|
64
|
+
.nav-sidebar > .active > a:focus {
|
65
|
+
color: #fff;
|
66
|
+
background-color: #428bca;
|
67
|
+
}
|
68
|
+
|
69
|
+
|
70
|
+
/*
|
71
|
+
* Main content
|
72
|
+
*/
|
73
|
+
|
74
|
+
.main {
|
75
|
+
padding: 20px;
|
76
|
+
}
|
77
|
+
@media (min-width: 768px) {
|
78
|
+
.main {
|
79
|
+
padding-right: 40px;
|
80
|
+
padding-left: 40px;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
.main .page-header {
|
84
|
+
margin-top: 0;
|
85
|
+
}
|
86
|
+
|
87
|
+
|
88
|
+
/*
|
89
|
+
* Placeholder dashboard ideas
|
90
|
+
*/
|
91
|
+
|
92
|
+
.placeholders {
|
93
|
+
margin-bottom: 30px;
|
94
|
+
text-align: center;
|
95
|
+
}
|
96
|
+
.placeholders h4 {
|
97
|
+
margin-bottom: 0;
|
98
|
+
}
|
99
|
+
.placeholder {
|
100
|
+
margin-bottom: 20px;
|
101
|
+
}
|
102
|
+
.placeholder img {
|
103
|
+
display: inline-block;
|
104
|
+
border-radius: 50%;
|
105
|
+
}
|