mail_daemon 0.0.5
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.
- 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
|
+
}
|