mail_daemon 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 48feab4732334f1c8c9286c661a700b603ef6189
4
+ data.tar.gz: 410115f6ee8a2e51c3a46907b5639ceb160c8810
5
+ SHA512:
6
+ metadata.gz: 6b41e1e61aaca95d8f32e5967b4d2a488dd292f8f211ebb3d33530045121307be75c667026a2a6dab1b1219eb172dfc02efda21a5d482333b7bf06801a9a02ca
7
+ data.tar.gz: f3458d0f73b2df26d6a119a89680aabc4cb7f8248769c8b46cf04d4eaa8941b6ef9c2d1ad761b4f56dc5031460ef54dbb6e8aa3af87660c4bf5ad5f7c99a3f5c
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ *.swp
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ mail_daemon
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.2.3
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mail_daemon.gemspec
4
+ gemspec
5
+
6
+ gem "mail_room", :path => "/Users/stewartmckee/code/mail_room"
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 EmergeAdapt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Ijonas Kisselbach
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # mail_daemon
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'mail_daemon'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install mail_daemon
18
+
19
+ ## Design
20
+
21
+ ###Gems
22
+
23
+ * mail_room - retrieves mail from multiple mailboxes at once and sends them through sidekiq or an http postback
24
+ * mail - mail parsing and sending gem
25
+ * talon - python code to extract the relevant body of the message
26
+
27
+
28
+ ### Notes
29
+
30
+ * config for office365 smtp needs authentication type 'login' gmail is 'plain'
31
+ * gmail apps authentication is by ip address only, suggest a feature of being able to host the send daemon on another box
32
+
33
+ ## Usage
34
+
35
+ Ensure the following environment variables are set:
36
+
37
+ REDIS_URL=redis://localhost:6379
38
+ CB_API_ENDPOINT=https://login.caseblocks.com
39
+ CB_API_TOKEN=...
40
+
41
+ And run the daemon from the CLI
42
+
43
+ mail_daemon
44
+
45
+ ## Contributing
46
+
47
+ 1. Fork it ( https://github.com/[my-github-username]/mail_daemon/fork )
48
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
49
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
50
+ 4. Push to the branch (`git push origin my-new-feature`)
51
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/mail_daemon ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby -Ilib
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+ # Prepares the $LOAD_PATH by adding to it lib directories of the gem and
5
+ # its dependencies:
6
+ require 'mail_daemon'
7
+ require 'ap'
8
+
9
+
10
+ mailboxes = [
11
+ {:host => "imap.gmail.com", :username => "stewart@emergeadapt.com", :port => 993, :start_tls => true, :password => "junction1", :ssl => true}
12
+ ]
13
+
14
+ handler = MailDaemon::Handler.new(:connections => mailboxes)
15
+
16
+ handler.start do |message, type|
17
+ if type == "status_update"
18
+ puts "#{message[:status][0].upcase}#{message[:status][1..-1]} #{message[:mailbox][:username]}"
19
+ else
20
+ ap message
21
+ end
22
+ end
data/config.yml ADDED
@@ -0,0 +1,27 @@
1
+ ---
2
+ :mailboxes:
3
+ -
4
+ :email: "stewart@activeinformationdesign.com"
5
+ :password: "junction1"
6
+ :name: "inbox"
7
+ :search_command: 'UNSEEN'
8
+ :delivery_method: sidekiq
9
+ :delivery_options:
10
+ :worker: EmailHandlerWorker
11
+ -
12
+ :email: "stewart@emergeadapt.com"
13
+ :password: "junction1"
14
+ :name: "inbox"
15
+ :search_command: 'UNSEEN'
16
+ :delivery_method: sidekiq
17
+ :delivery_options:
18
+ :worker: EmailHandlerWorker
19
+ -
20
+ :email: "stewartmckee@emergeadapttest.onmicrosoft.com"
21
+ :host: "outlook.office365.com"
22
+ :password: "Katr1n33"
23
+ :name: "inbox"
24
+ :search_command: 'UNSEEN'
25
+ :delivery_method: sidekiq
26
+ :delivery_options:
27
+ :worker: EmailHandlerWorker
@@ -0,0 +1,71 @@
1
+ require_relative 'mail_daemon/version'
2
+ require_relative 'mail_daemon/helpers'
3
+ require_relative 'mail_daemon/email_body_parser'
4
+ require_relative 'mail_daemon/email_handler'
5
+ require_relative 'mail_daemon/imap/statuses'
6
+ require_relative 'mail_daemon/imap/connection'
7
+ require_relative 'mail_daemon/imap/watcher'
8
+ require_relative 'mail_daemon/encryption'
9
+ require 'sinatra/base'
10
+ require 'json'
11
+ require 'open3'
12
+ require 'mail'
13
+
14
+ include MailDaemon::Helpers
15
+
16
+ module MailDaemon
17
+ class Handler
18
+ def initialize(options)
19
+ setup_options(options)
20
+ default_option :connections, []
21
+
22
+ Signal.trap("INT") {
23
+ Thread.new {self.stop}.join
24
+ }
25
+ # Trap `Kill `
26
+ Signal.trap("TERM") {
27
+ Thread.new {self.stop}.join
28
+ }
29
+ end
30
+
31
+ def restart
32
+ self.stop if self.running?
33
+ self.reload unless @mailbox_config
34
+ self.start
35
+ end
36
+
37
+ def running?
38
+ @watchers && @watchers.detect{|watcher| watcher.running?}
39
+ end
40
+
41
+ # Signal catching
42
+ def stop
43
+ @watchers.map{|watcher| watcher.stop}
44
+ @threads.map{|t| t.kill }
45
+ end
46
+
47
+ def start(&block)
48
+ @watchers = []
49
+
50
+ @options[:connections].each do |mailbox|
51
+ mailbox[:ssl_options] = {:verify_mode => OpenSSL::SSL::VERIFY_NONE} if mailbox[:ssl]
52
+
53
+ puts "Setting up watcher for #{mailbox[:username]}"
54
+ @watchers << MailDaemon::Imap::Watcher.new(mailbox)
55
+ end
56
+ @threads = []
57
+ @watchers.each do |watcher|
58
+ @threads << Thread.new do
59
+ watcher.start do |message|
60
+ type = message.delete(:type)
61
+ yield message, type
62
+ end
63
+ end
64
+ end
65
+
66
+ @threads.map{|t| t.join}
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,28 @@
1
+ require 'open3'
2
+
3
+ module MailDaemon
4
+ class EmailBodyParser
5
+ def self.parse(text, type="text/plain")
6
+
7
+ data_hash = {:body => text, :type => type}
8
+
9
+ json_hash = data_hash.to_json
10
+
11
+ stdin, stdout, stderr, wait_thr = Open3.popen3('python', '/Users/stewartmckee/code/talon/test.py')
12
+ stdin.write(json_hash)
13
+ stdin.close
14
+ result = stdout.read
15
+ stdout.close
16
+ error = stderr.read
17
+ stderr.close
18
+ exit_code = wait_thr.value
19
+ if exit_code == 0
20
+ result
21
+ else
22
+ puts result
23
+ raise error
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,109 @@
1
+ require 'fileutils'
2
+ require 'mail'
3
+ require_relative "email_body_parser"
4
+ require 'nokogiri'
5
+ require 'sanitize'
6
+
7
+ module MailDaemon
8
+ class EmailHandler
9
+ def initialize(options)
10
+ @options = options
11
+ @email = Mail.read_from_string(options[:inbound_message])
12
+
13
+ puts "Handling inbound email from #{@email.from}"
14
+
15
+ if @email.multipart?
16
+ @email.parts.each do |part|
17
+ content_type = part.content_type.split(";")[0]
18
+ if content_type[0..3] == "text"
19
+ if content_type == "text/html"
20
+ @html_body = part.body.decoded.force_encoding("ASCII-8BIT").encode('UTF-8', undef: :replace, replace: '')
21
+ elsif content_type == "text/plain"
22
+ @text_body = part.body.decoded.force_encoding("ASCII-8BIT").encode('UTF-8', undef: :replace, replace: '')
23
+ end
24
+ end
25
+ end
26
+ else
27
+ @text_body = @email.body.decoded.force_encoding("ASCII-8BIT").encode('UTF-8', undef: :replace, replace: '')
28
+ end
29
+
30
+ @body = @html_body || @text_body
31
+ end
32
+
33
+ def generate_message_hash
34
+
35
+ message_hash = {
36
+ "from" => @email.from,
37
+ "to" => @email.to,
38
+ "subject" => @email.subject,
39
+ "body" => @body,
40
+ "attachements" => prepare_attachments,
41
+ "metadata" => fetch_metadata
42
+ }
43
+ message_hash.delete(:inbound_message)
44
+ message_hash
45
+ end
46
+
47
+ def fetch_metadata
48
+ fetch_fingerprints.merge({
49
+ "references" => references
50
+ })
51
+ end
52
+
53
+ def references
54
+ @email.header["References"] ? @email.header["References"].field.element.message_ids : []
55
+ end
56
+
57
+ def fetch_fingerprints
58
+ previous_id = references.last
59
+ match = /(.*?)\.(.*?)@emergeadapt.com/.match(previous_id)
60
+ if match
61
+ if match[2] == "case"
62
+ {"case_id" => match[1]}
63
+ else
64
+ {"message_id" => match[1]}
65
+ end
66
+ else
67
+ {}
68
+ end
69
+ end
70
+
71
+ def prepare_attachments
72
+ attachments = []
73
+ @email.attachments.each do | attachment |
74
+ filename = attachment.filename
75
+ random_tag = Random.rand(100000000000)
76
+ attachment_folder = File.join(["/", "tmp", "caseblocks", @options[:mailbox][:account_code], "attachments"])
77
+ FileUtils::mkdir_p(attachment_folder)
78
+ file_path = File.join([attachment_folder, "#{random_tag}.#{filename}"])
79
+ File.open(file_path, "w+b", 0644) {|f| f.write attachment.body.decoded}
80
+ attachments << {
81
+ "filename" => attachment.filename,
82
+ "filepath" => file_path
83
+ }
84
+ end
85
+ attachments
86
+ end
87
+
88
+
89
+ def content_type
90
+ @html_body.nil? ? "text/plain" : "text/html"
91
+ end
92
+
93
+ def sanitize!
94
+ @body = Sanitize.fragment(@body, Sanitize::Config::RELAXED)
95
+ end
96
+
97
+ def clean_replies!
98
+ @body = MailDaemon::EmailBodyParser.parse(@body, content_type)
99
+ end
100
+
101
+ def queue!
102
+ $redis
103
+ end
104
+
105
+ def body
106
+ @body
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,155 @@
1
+ require 'redis'
2
+ require 'ap'
3
+ require 'mysql2'
4
+
5
+ module MailDaemon
6
+ class EmailWatcher
7
+
8
+ def initialize
9
+
10
+ raise "REDIS_URL environment variable is required (eg redis://localhost:6739)" unless ENV["REDIS_URL"]
11
+ raise "MYSQL_HOST environment variable is required" unless ENV["MYSQL_HOST"]
12
+ raise "MYSQL_DATABASE environment variable is required" unless ENV["MYSQL_DATABASE"]
13
+ raise "MYSQL_USERNAME environment variable is required" unless ENV["MYSQL_USERNAME"]
14
+ raise "MYSQL_PASSWORD environment variable is required" unless ENV["MYSQL_PASSWORD"]
15
+
16
+ ENV["MYSQL_PASSWORD"] = "" unless ENV["MYSQL_PASSWORD"]
17
+ ENV["MYSQL_PORT"] = "3306"
18
+
19
+ redis_url = URI.parse(ENV["REDIS_URL"])
20
+
21
+ $redis = Redis.new(:host => redis_url.host, :port => redis_url.port)
22
+
23
+ Signal.trap("INT") {
24
+ Thread.new {self.stop}.join
25
+ }
26
+ # Trap `Kill `
27
+ Signal.trap("TERM") {
28
+ Thread.new {self.stop}.join
29
+ }
30
+
31
+
32
+
33
+ start
34
+ end
35
+
36
+ def configuration
37
+ mailboxes = []
38
+ mysql_client do |mysql|
39
+ statement = mysql.prepare("SELECT case_blocks_email_accounts.id, case_blocks_accounts.nickname, case_blocks_email_accounts.imap_username, case_blocks_email_accounts.imap_encrypted_password, case_blocks_email_accounts.imap_host, case_blocks_email_accounts.imap_port, case_blocks_email_accounts.imap_ssl, case_blocks_email_accounts.imap_start_tls, case_blocks_email_accounts.imap_folder_name, case_blocks_email_accounts.imap_search_command, case_blocks_email_accounts.imap_messages_processed, case_blocks_email_accounts.imap_last_processed_at FROM case_blocks_email_accounts JOIN case_blocks_accounts ON case_blocks_email_accounts.account_id = case_blocks_accounts.id where imap_enabled=1")
40
+ result = statement.execute()
41
+ result.each do |row|
42
+ ssl_options = row["imap_ssl"]==1 ? {:verify_mode => OpenSSL::SSL::VERIFY_NONE} : false
43
+ decrypted_password = Encryption.new.decrypt(row["imap_encrypted_password"])
44
+ mailboxes << {:id => row["id"], :account_code => row["nickname"], :username => row["imap_username"], :host => row["imap_host"], :port => row["imap_port"], :password => decrypted_password, :ssl_options => ssl_options, :start_tls => row["imap_start_tls"]==1, :name => row["imap_folder_nam"], :search_command => row["imap_search_command"], :message_count => row["imap_messages_processed"], :last_delivered_at => row["imap_last_processed_at"]}
45
+ end
46
+ end
47
+ mailboxes
48
+ end
49
+
50
+ def restart
51
+ self.stop if self.running?
52
+ self.reload unless @mailbox_config
53
+ self.start
54
+ end
55
+
56
+ def running?
57
+ @watchers.detect{|watcher| watcher.running?}
58
+ end
59
+
60
+ # Signal catching
61
+ def stop
62
+ @watchers.map{|watcher| watcher.stop}
63
+ @threads.map{|t| t.kill }
64
+ end
65
+
66
+ def start
67
+ @watchers = []
68
+
69
+ configuration.each do |mailbox|
70
+ puts "Setting up watcher for #{mailbox[:username]}"
71
+ @watchers << MailDaemon::Imap::Watcher.new(mailbox)
72
+ end
73
+
74
+ @threads = []
75
+ @watchers.each do |watcher|
76
+ @threads << Thread.new do
77
+ watcher.start do |message|
78
+ if message[:type] == "status_update"
79
+ update_status(message)
80
+ elsif message[:type] == "incoming_email"
81
+ handle_incoming_message(message)
82
+ else
83
+ puts "Unknown message type"
84
+ ap message
85
+ end
86
+ end
87
+ end
88
+ end
89
+ @threads.map{|t| t.join}
90
+
91
+ end
92
+
93
+ def handle_incoming_message(options)
94
+ @handler = MailDaemon::EmailHandler.new(options)
95
+ @handler.sanitize!
96
+ @handler.clean_replies!
97
+
98
+ hash = @handler.generate_message_hash
99
+
100
+ $redis.lpush(redis_key(options), hash)
101
+ puts "Queued."
102
+ end
103
+
104
+ def redis_key(options)
105
+ "cb:comms:#{options[:mailbox][:account_code]}:inbox"
106
+ end
107
+
108
+ def update_status(options)
109
+ puts "Status Update: #{options[:status]} for #{options[:mailbox][:username]}"
110
+ mysql_client do |mysql|
111
+ status_message = options[:status]
112
+ statement = mysql.prepare("UPDATE case_blocks_email_accounts SET imap_status='#{options[:status]}', imap_status_message='#{status_message}' where id=?")
113
+ statement.execute(options[:mailbox][:id])
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def mysql_client(&block)
120
+ client = Mysql2::Client.new(:host => ENV["MYSQL_HOST"], :port => ENV["MYSQL_PORT"], :username => ENV["MYSQL_USERNAME"], :password => ENV["MYSQL_PASSWORD"], :database => ENV["MYSQL_DATABASE"])
121
+ yield client
122
+ client.close
123
+ end
124
+
125
+ def mailbox_status_message(watcher)
126
+ if watcher.logging_in?
127
+ "Connecting and Logging into server"
128
+ elsif watcher.logged_in?
129
+ "Connected"
130
+ else
131
+ "Disconnected"
132
+ end
133
+ end
134
+ def mailbox_status(watcher)
135
+ if watcher.logging_in?
136
+ :logging_in
137
+ elsif watcher.logged_in?
138
+ :connected
139
+ else
140
+ :disconnected
141
+ end
142
+ end
143
+ def mailbox_status_colour(watcher)
144
+ if watcher.logging_in?
145
+ "orange"
146
+ elsif watcher.logged_in?
147
+ "green"
148
+ else
149
+ "red"
150
+ end
151
+ end
152
+
153
+
154
+ end
155
+ end