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.
@@ -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
+ }