mail_daemon 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module MailDaemon
2
+ VERSION = "0.0.5"
3
+ 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
+ }