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 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