navi-email-sync 0.0.1

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
+ SHA256:
3
+ metadata.gz: 2e7072e4c9597d5cb3611cba10a7a303972b40c122a2f732847c31882c750eac
4
+ data.tar.gz: f0eca6f0038deb376c18021e51899b3a98a90689728ea25058ad773940beb1da
5
+ SHA512:
6
+ metadata.gz: 79a7c138e91b5362d60c09167a156c37b89e93ade9912d2d7851fdf9ef09895ff18be1e506f0542fd19ecc8f74e0413cff5bdd57ede154b82dde7c78e26492f7
7
+ data.tar.gz: 0725a0798c776c679f4809d252eab8c671e1f439140c5b00b29c61862849a92cd7626d4ff8e58667895b58e42cc81901f99ca9ce79f0b369e8c9db88d934c98b
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .idea/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.11.2
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at surya@whitehatengineering.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in navi_client.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Surya
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # NaviClient
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/navi_client`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Generate Documentation
8
+ ```
9
+ $ rdoc
10
+ ```
11
+ Browse html file inside the ```doc``` folder
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'navi_client'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install navi_client
28
+
29
+ ## Usage
30
+
31
+ TODO: Write usage instructions here
32
+
33
+ ## Development
34
+
35
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
+
37
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
38
+
39
+ ## Contributing
40
+
41
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/navi_client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
42
+
43
+
44
+ ## License
45
+
46
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/.DS_Store ADDED
Binary file
data/bin/console ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ # You can add fixtures and/or initialization code here to make experimenting
5
+ # with your gem easier. You can also use a different console, if you like.
6
+
7
+ # (If you use this, don't forget to add pry to your Gemfile!)
8
+ # require "pry"
9
+ # Pry.start
10
+
11
+ require "irb"
12
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,9 @@
1
+ # Just copy paste the following format in application.yml and configure accordingly
2
+
3
+ development:
4
+ api_url: http://localhost:3020/
5
+ parser_url: http://localhost:9090
6
+
7
+ production:
8
+ parser_url: http://34.214.134.104:9090
9
+
data/lib/client.rb ADDED
@@ -0,0 +1,250 @@
1
+ require "net/imap"
2
+ require "mail"
3
+ require "time"
4
+
5
+ require 'base64'
6
+ require 'fileutils'
7
+ require 'yaml'
8
+
9
+ require "logger"
10
+
11
+ require "httparty"
12
+ require "http_service/naviai"
13
+ require 'openssl'
14
+
15
+ ##
16
+ # This module provides the common functionality that will be needed for local and cloud module.
17
+ # @client_type defines if the gem is being used for local-lockbox or cloud-version.
18
+ #
19
+ module Client
20
+
21
+ attr_accessor :downloaded
22
+
23
+ attr_accessor :message_ids
24
+
25
+ attr_accessor :total_emails
26
+
27
+ @logglyTag
28
+
29
+ def logger
30
+ @logger
31
+ end
32
+
33
+ def errors
34
+ @errors
35
+ end
36
+
37
+ #
38
+ # imap_connection
39
+ #
40
+ # connect the app with imap server
41
+ #
42
+ def imap_connection(server, username, password, exitOnFail = true)
43
+ # connect to IMAP server
44
+ @imap = Net::IMAP.new server, ssl: true, certs: nil, verify: false
45
+
46
+ Net::IMAP.debug = @net_imap_debug
47
+
48
+ # http://ruby-doc.org/stdlib-2.1.5/libdoc/net/imap/rdoc/Net/IMAP.html#method-i-capability
49
+ capabilities = @imap.capability
50
+
51
+ @logger.debug("imap capabilities: #{capabilities.join(',')}") if @debug
52
+
53
+ if @client_type == 'local'
54
+ unless capabilities.include? "IDLE" || @debug
55
+ @logger.info "'IDLE' IMAP capability not available in server: #{server}"
56
+ @imap.disconnect
57
+ exit if exitOnFail
58
+ end
59
+ end
60
+
61
+ begin
62
+ # login
63
+ @imap.login username, password
64
+ @errors = nil
65
+ rescue Net::IMAP::NoResponseError => e
66
+ # mostly due to credentials error
67
+ @errors = {exception: e}
68
+ rescue Net::IMAP::BadResponseError => e
69
+ @errors = {exception: e}
70
+ rescue
71
+ @errors = {exception: e}
72
+ end
73
+
74
+ # return IMAP connection handler
75
+ @imap
76
+ end
77
+
78
+ #
79
+ # retrieve_emails
80
+ #
81
+ # retrieve any mail from a folder, followin specified serach condition
82
+ # for any mail retrieved call a specified block
83
+ #
84
+ def fetch_emails(&process_email_block)
85
+
86
+ @message_ids.each_with_index do |message_id, i|
87
+ begin
88
+ # fetch all the email contents
89
+ data = @imap.uid_fetch(message_id, "RFC822")
90
+ data.each do |d|
91
+ msg = d.attr['RFC822']
92
+ # instantiate a Mail object to avoid further IMAP parameters nightmares
93
+ mail = Mail.read_from_string msg
94
+
95
+ # call the block with mail object as param
96
+ process_email_block.call mail, i, i == @message_ids.length-1, message_id
97
+
98
+ # mark as read
99
+ if @mark_as_read
100
+ @imap.store(message_id, "+FLAGS", [:Seen])
101
+ end
102
+ end
103
+ rescue => e
104
+ unless logger.nil?
105
+ logger.info "Issue processing email for uuid ##{message_id}, #{e.message}"
106
+ end
107
+
108
+ logToLoggly({event:"EMAIL_SYNC_FAILED", env: @env, storage: @client_type, email: @current_user_email, uuid: message_id, error: e.message})
109
+ raise e
110
+ end
111
+ end
112
+ end
113
+
114
+
115
+ def init_messages label, search_condition = ['ALL']
116
+
117
+ # examine will read email and sets flags unread
118
+ # https://stackoverflow.com/questions/16516464/read-gmail-xoauth-mails-without-marking-it-read
119
+ @imap.examine label
120
+
121
+ @message_ids = @imap.uid_search(search_condition)
122
+ @total_emails = @message_ids.length
123
+ downloaded_email = getMessageUUIds("#{@download_path}meta/")
124
+ downloaded_email = downloaded_email.collect{|i| i.to_i}
125
+
126
+ @downloaded = downloaded_email.length
127
+ @downloaded = @downloaded - 2 if @downloaded > 1
128
+ @downloaded = [@downloaded, @total_emails].min
129
+
130
+ @message_ids = @message_ids - downloaded_email
131
+ @message_ids = @message_ids&.sort {|msg_a, msg_b| msg_b <=> msg_a}
132
+
133
+ if @debug
134
+ if @message_ids.empty?
135
+ @logger.info "No new emails found..."
136
+ elsif downloaded_email.any?
137
+ @logger.info "Found emails saved in your #{@client_type}. Downloading only #{@message_ids.count} new emails..."
138
+ else
139
+ @logger.info "Downloading #{@message_ids.count} emails."
140
+ end
141
+ end
142
+
143
+ end
144
+
145
+ ##
146
+ # Process each email downloaded from imap-server and creates meta file that will be sent over to the navi-ai service.
147
+ # Meta will content information like: ['from', 'to', 'cc', ..., 'body_url']. This information is then used by navi-ai to parse the body content.
148
+ #
149
+ def process_email(mail, uid, stamp = nil)
150
+ meta = Hash.new
151
+ custom_uid = (Time.now.to_f * 1000).to_s + "_" + mail.__id__.to_s
152
+
153
+ meta["from"] = mail[:from].to_s
154
+ meta["to"] = mail[:to].to_s
155
+ meta["cc"] = mail[:cc].to_s
156
+ meta["subject"] = mail.subject
157
+ meta["date"] = mail.date.to_s
158
+
159
+
160
+ if mail.multipart?
161
+ for i in 0...mail.parts.length
162
+ m = download(mail.parts[i], custom_uid)
163
+ meta.merge!(m) unless m.nil?
164
+ end
165
+ else
166
+ m = download(mail, custom_uid)
167
+ meta.merge!(m) unless m.nil?
168
+ end
169
+
170
+ if stamp.nil?
171
+ save(meta, "meta/#{uid.to_s + '_' + custom_uid}")
172
+ else
173
+ save(meta, "meta/#{uid.to_s + '_' + custom_uid}", stamp)
174
+ end
175
+
176
+ end
177
+
178
+ def encrypt(data)
179
+ Base64.encode64(data)
180
+ end
181
+
182
+ def depcrecated_encrypt(data)
183
+ cipher = OpenSSL::Cipher::AES.new(256, :CFB)
184
+ cipher.encrypt
185
+
186
+ yml_config = config
187
+
188
+ key_iv_exists = (yml_config['key'] && yml_config['iv']) ? (!yml_config['key'].empty? && !yml_config['iv'].empty? ) : false
189
+
190
+ if key_iv_exists
191
+ # this condition must be true for cloud version
192
+ cipher.key = Base64.decode64(File.read(yml_config['key']))
193
+ cipher.iv = Base64.decode64(File.read(yml_config['iv']))
194
+ else
195
+ cipher.key = key = cipher.random_key
196
+ cipher.iv = iv = cipher.random_iv
197
+
198
+ key_path, iv_path = save_aes_key_iv(key, iv)
199
+
200
+ yml_config['key'] = key_path
201
+ yml_config['iv'] = iv_path
202
+
203
+ update_config(yml_config)
204
+ end
205
+
206
+ encrypted = cipher.update(data)
207
+ Base64.encode64(encrypted)
208
+ end
209
+
210
+ def time_now
211
+ Time.now.utc.iso8601(3)
212
+ end
213
+
214
+ ##
215
+ # If the gem is being used for local-lockbox mode, after fetching all emails, the process will go idle mode. If user send the interrupt command, it will call shutdown to disconnect the connection with imap server
216
+ #
217
+ def shutdown(imap)
218
+ imap.idle_done
219
+ imap.logout unless imap.disconnected?
220
+ imap.disconnect
221
+
222
+ @logger.info "#{$0} has ended (crowd applauds)"
223
+ exit 0
224
+ end
225
+
226
+ def logToLoggly(messageBody)
227
+
228
+ if(@logglyTag)
229
+ begin
230
+ HTTParty.post("http://logs-01.loggly.com/bulk/0d67f93b-6568-4b00-9eca-97d0ea0bd5a1/tag/#{@logglyTag}/",
231
+ body: messageBody.to_json,
232
+ headers: { 'Content-Type' => 'application/json' } )
233
+ rescue
234
+ true
235
+ end
236
+
237
+ else
238
+ if @debug && !@logger.nil?
239
+ @logger.info "Logging to Loggly disabled"
240
+ @logger.info messageBody
241
+ end
242
+ end
243
+
244
+ end
245
+
246
+ def setupLoggly(tag)
247
+ @logglyTag = tag
248
+ end
249
+
250
+ end
@@ -0,0 +1,140 @@
1
+ require Gem::Specification.find_by_name("navi_client").gem_dir+"/lib/client"
2
+ require 'concurrent'
3
+
4
+ module NaviClient
5
+
6
+ ##
7
+ # This class represents the client for cloud version.
8
+ # In cloud version, all the content will be saved and also being used from the s3
9
+ #
10
+ class Cloud
11
+ include Concurrent::Async
12
+ include Client
13
+
14
+ attr_reader :id
15
+
16
+ def initialize(sso_web_url = ENV['api_url'], env = Rails.env)
17
+ super()
18
+
19
+ # client-id used to track the process
20
+ @id = SecureRandom.uuid
21
+
22
+ # flag to print Ruby library debug info (very detailed)
23
+ @net_imap_debug = false
24
+
25
+ # flag to mark email as read after gets downloaded.
26
+ @mark_as_read = false
27
+ # flag to turn on/off debug mode.
28
+ @debug = false
29
+
30
+ @logger = nil
31
+
32
+ # sso_web (authentication) config.
33
+ @sso_web_url = sso_web_url
34
+ # authentication token received from sso_web used to authenticate the request to database_api
35
+ @token = nil
36
+
37
+ # client_type
38
+ @client_type = "cloud"
39
+
40
+ @download_path = config[:s3_download_folder] + '/'
41
+
42
+ # set email_address of current_user for s3 folder name
43
+ @current_user_email = nil
44
+ @env = env
45
+ end
46
+
47
+ def override_logger(logger)
48
+ @logger = logger
49
+ end
50
+
51
+ #
52
+ # login
53
+ #
54
+ # login to the navi-cloud and get the authentication token
55
+ #
56
+ def login(session_token)
57
+ @token = session_token
58
+ end
59
+
60
+ def set_current_user_email(email)
61
+ @current_user_email = email
62
+ @download_path = config[:s3_download_folder] + '/' + email + "/"
63
+ end
64
+
65
+ ##
66
+ # send bulk request to navi-ai service with list of input files downloaded from imap server.
67
+ #
68
+ def send_request(in_filenames = [], is_last: false)
69
+ unless in_filenames.blank?
70
+ download_path = config['s3_download_folder'] + "/" + @current_user_email
71
+ filepath = download_path + "/inputs/" + (Time.now.to_f * 1000).to_s
72
+ filename = upload_to_s3(filepath, in_filenames.join("\n"))
73
+
74
+ HTTPService::NaviAI.start(filepath: filename, client_id: @id, client_type: @client_type, token: @token, email: @current_user_email, is_last: is_last)
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Downloads the email content from imap-server and save it to the download_path
80
+ #
81
+ def download(message, custom_uid)
82
+ download_path = config[:s3_download_folder] + "/" + @current_user_email
83
+ if ['text/plain', 'text/html'].include? message.mime_type
84
+
85
+ h = Hash.new
86
+ out_file = download_path + "/" + message.mime_type + "/"+custom_uid
87
+
88
+ s3_filepath = upload_to_s3(out_file, encrypt(message.decoded))
89
+ key = message.mime_type.split("/").join("_")
90
+
91
+ h[key] = s3_filepath
92
+ return h
93
+ end
94
+ end
95
+
96
+ ##
97
+ # save data to download_path with file named by filename params.
98
+ # Input is hashmap, and it save the hashmap as yml format.
99
+ #
100
+ def save(data={}, filename)
101
+ download_path = config[:s3_download_folder] + "/" + @current_user_email
102
+ filepath = download_path + "/" + filename + ".yml"
103
+
104
+ return upload_to_s3(filepath, data.to_yaml)
105
+ end
106
+
107
+ ##
108
+ # Helper function to upload the content to the s3
109
+ def upload_to_s3(file_path, content)
110
+ credentials = Aws::Credentials.new(config[:aws_key], config[:aws_secret])
111
+ s3 = Aws::S3::Client.new(credentials: credentials, region: config[:aws_region])
112
+ obj = s3.put_object({
113
+ body: content,
114
+ bucket: config[:s3_bucket],
115
+ key: file_path
116
+ })
117
+ return file_path if obj.successful?
118
+ return ""
119
+ end
120
+
121
+ def s3_resource_files(bucket_name, prefix)
122
+ files = []
123
+ credentials = Aws::Credentials.new(config[:aws_key], config[:aws_secret])
124
+ s3 = Aws::S3::Resource.new(credentials: credentials, region: config[:aws_region])
125
+ s3.bucket(bucket_name).objects(prefix: prefix).each do |obj|
126
+ files << obj.key
127
+ end
128
+ return files
129
+ end
130
+
131
+ def getMessageUUIds(prefix)
132
+ files = s3_resource_files(config[:s3_bucket], prefix)
133
+ files.empty? ? [] : files.map { |i| i.empty? ? 0 : i.split('/').last.split("_").first.to_i }
134
+ end
135
+
136
+ def config
137
+ YAML.load_file(Rails.root.join("config/navi_client.yml")).with_indifferent_access
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,222 @@
1
+ require "mail"
2
+ require "time"
3
+
4
+ require 'base64'
5
+ require 'fileutils'
6
+ require 'yaml'
7
+
8
+ require "logger"
9
+
10
+ require "httparty"
11
+ require "http_service/naviai"
12
+ require 'openssl'
13
+ require 'google/apis/gmail_v1'
14
+ require 'googleauth'
15
+ require 'googleauth/stores/file_token_store'
16
+
17
+ ##
18
+ # This module provides the common functionality that will be needed for local and cloud module.
19
+ # @client_type defines if the gem is being used for local-lockbox or cloud-version.
20
+ #
21
+ module GmailClient
22
+
23
+ OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'.freeze
24
+ APPLICATION_NAME = 'Gmail Api Sync'.freeze
25
+
26
+ attr_accessor :downloaded
27
+
28
+ attr_accessor :message_ids
29
+
30
+ attr_accessor :total_emails
31
+
32
+ @logglyTag
33
+
34
+ def logger
35
+ @logger
36
+ end
37
+
38
+ def errors
39
+ @errors
40
+ end
41
+
42
+ def init client_path, user_cred_path, user_id
43
+
44
+ client_id = Google::Auth::ClientId.from_file(client_path)
45
+ token_store = Google::Auth::Stores::FileTokenStore.new(file: user_cred_path)
46
+ @authorizer = Google::Auth::UserAuthorizer.new(client_id, Google::Apis::GmailV1::AUTH_GMAIL_READONLY, token_store)
47
+
48
+ credentials = @authorizer.get_credentials(user_id)
49
+
50
+ unless credentials.nil?
51
+ init_apis credentials
52
+ end
53
+ credentials
54
+ end
55
+
56
+ def authorize code, user_id
57
+ credentials = @authorizer.get_and_store_credentials_from_code(
58
+ user_id: user_id, code: code, base_url: OOB_URI
59
+ )
60
+ unless credentials.nil?
61
+ init_apis credentials
62
+ end
63
+ credentials
64
+ end
65
+
66
+ def init_apis cred
67
+ @service = Google::Apis::GmailV1::GmailService.new
68
+ @service.client_options.application_name = APPLICATION_NAME
69
+ @service.authorization = cred
70
+ end
71
+
72
+ def authorization_url
73
+ @authorizer.get_authorization_url(base_url: OOB_URI)
74
+ end
75
+
76
+ #
77
+ # retrieve_emails
78
+ #
79
+ # retrieve any mail from a folder, followin specified serach condition
80
+ # for any mail retrieved call a specified block
81
+ #
82
+ def fetch_emails(&process_email_block)
83
+
84
+ @message_ids.each_with_index do |message_id, i|
85
+ begin
86
+ # fetch all the email contents
87
+ # data = imap.uid_fetch(message_id, "RFC822")
88
+ result = @service.get_user_message('me', message_id)
89
+
90
+
91
+ process_email_block.call result, i, i == @message_ids.length-1, message_id
92
+
93
+ rescue => e
94
+ unless logger.nil?
95
+ logger.info "Issue processing email for uuid ##{message_id}, #{e.message}"
96
+ end
97
+
98
+ logToLoggly({event:"EMAIL_SYNC_FAILED", env: @env, storage: @client_type, email: @current_user_email, uuid: message_id, error: e.message})
99
+ raise e
100
+ end
101
+ end
102
+
103
+ end
104
+
105
+
106
+ ##
107
+ # Process each email downloaded from imap-server and creates meta file that will be sent over to the navi-ai service.
108
+ # Meta will content information like: ['from', 'to', 'cc', ..., 'body_url']. This information is then used by navi-ai to parse the body content.
109
+ #
110
+ def process_email(mail, uid, stamp = nil)
111
+ meta = Hash.new
112
+ custom_uid = (Time.now.to_f * 1000).to_s + "_" + uid
113
+
114
+ payload = mail.payload
115
+
116
+ headers = payload.headers
117
+ date = headers.any? { |h| h.name == 'Date' } ? headers.find { |h| h.name == 'Date' }.value : ''
118
+ from = headers.any? { |h| h.name == 'From' } ? headers.find { |h| h.name == 'From' }.value : ''
119
+ to = headers.any? { |h| h.name == 'To' } ? headers.find { |h| h.name == 'To' }.value : ''
120
+ subject = headers.any? { |h| h.name == 'Subject' } ? headers.find { |h| h.name == 'Subject' }.value : ''
121
+ cc = headers.any? { |h| h.name == 'Cc' } ? headers.find { |h| h.name == 'Cc' }.value : ''
122
+
123
+ meta["from"] = from
124
+ meta["to"] = to
125
+ meta["cc"] = cc
126
+ meta["subject"] = subject
127
+ meta["date"] = date
128
+
129
+ if payload.parts&.any?
130
+ payload.parts.map { |part|
131
+ m = download({"decoded" => part.body.data, "mime_type" => part.mime_type}, custom_uid)
132
+ meta.merge!(m) unless m.nil?
133
+ }
134
+ else
135
+ m = download({"decoded" => payload.body.data, "mime_type" => "text/html"}, custom_uid)
136
+ meta.merge!(m) unless m.nil?
137
+ end
138
+
139
+ if stamp.nil?
140
+ save(meta, "meta/#{uid.to_s + '_' + custom_uid}")
141
+ else
142
+ save(meta, "meta/#{uid.to_s + '_' + custom_uid}", stamp)
143
+ end
144
+
145
+ end
146
+
147
+ def all_messages label
148
+ messages = []
149
+ next_page = nil
150
+
151
+ begin
152
+ result = @service.list_user_messages('me', max_results: 1000, page_token: next_page, label_ids: [label])
153
+ messages += result.messages
154
+ # break if messages.size >= 1
155
+ next_page = result.next_page_token
156
+ end while next_page
157
+
158
+ messages
159
+ end
160
+
161
+ def get_user_info user_id
162
+ @service.get_user_profile user_id
163
+ end
164
+
165
+ def all_message_ids label
166
+ mids = []
167
+ messages = all_messages label
168
+ messages.each { |m| mids << m.id }
169
+ mids
170
+ end
171
+
172
+ def init_messages label
173
+ @message_ids = all_message_ids label
174
+ @total_emails = @message_ids.length
175
+ downloaded_email = getMessageUUIds("#{@download_path}meta/")
176
+
177
+ @downloaded = downloaded_email.length
178
+ @downloaded = @downloaded - 2 if @downloaded > 1
179
+ @downloaded = [@downloaded, @total_emails].min
180
+
181
+ @message_ids = @message_ids - downloaded_email
182
+
183
+ if @debug
184
+ if @message_ids.empty?
185
+ @logger.info "No new emails found..."
186
+ elsif downloaded_email.any?
187
+ @logger.info "Found emails saved in your #{@client_type}. Downloading only #{@message_ids.count} new emails..."
188
+ else
189
+ @logger.info "Downloading #{@message_ids.count} emails."
190
+ end
191
+ end
192
+
193
+ end
194
+
195
+ def logToLoggly(messageBody)
196
+
197
+ if(@logglyTag)
198
+ begin
199
+ HTTParty.post("http://logs-01.loggly.com/bulk/0d67f93b-6568-4b00-9eca-97d0ea0bd5a1/tag/#{@logglyTag}/",
200
+ body: messageBody.to_json,
201
+ headers: { 'Content-Type' => 'application/json' } )
202
+ rescue
203
+ true
204
+ end
205
+
206
+ else
207
+ if @debug && !@logger.nil?
208
+ @logger.info "Logging to Loggly disabled"
209
+ @logger.info messageBody
210
+ end
211
+ end
212
+
213
+ end
214
+
215
+ def setupLoggly(tag)
216
+ @logglyTag = tag
217
+ end
218
+
219
+ def encrypt(data)
220
+ Base64.encode64(data)
221
+ end
222
+ end
@@ -0,0 +1,15 @@
1
+ require "httparty"
2
+
3
+ module HTTPService
4
+ class NaviAI
5
+
6
+ def self.start(filepath:, client_id: nil, client_type:, token:, email:, is_last: false)
7
+ go_url = "#{ENV['parser_url']}/v2/metas"
8
+ HTTParty.post(go_url, body: { client_id: client_id, client_type: client_type, list_meta_path: filepath, token: token, email: email, is_last: is_last }.to_json, timeout: 400)
9
+ end
10
+
11
+ def self.generate_csv(url, token)
12
+ HTTParty.post(url, headers: {"Authorization": token})
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,245 @@
1
+ require_relative '../client'
2
+ require_relative '../gmail_client'
3
+
4
+ module NaviClient
5
+
6
+ ##
7
+ # This class represents the client for local lockbox mode.
8
+ # In local-lockbox mode, all the content will be saved and also being used from the local file system
9
+ #
10
+ class Local
11
+
12
+ CONFIG_PATH = File.join(ENV['HOME'], '/.navi/config.yml')
13
+
14
+ def initialize(method, client_type = "local", debug = true)
15
+ # flag to print Ruby library debug info (very detailed)
16
+ @net_imap_debug = false
17
+
18
+ # flag to mark email as read after gets downloaded.
19
+ @mark_as_read = false
20
+
21
+ # flag to turn on/off debug mode.
22
+ @debug = debug
23
+
24
+ # override the log file
25
+ mkdir_if_not_exist(config['client_log_file'])
26
+ @logger = Logger.new(config['client_log_file'])
27
+
28
+ # sso_web (authentication) config.
29
+ @sso_web_url = ENV['api_url']
30
+ # authentication token received from sso_web used to authenticate the request to database_api
31
+ @token = nil
32
+
33
+ @download_path = config['download_path'] + config['identifier'] + "/#{method}/"
34
+ @current_user_email = config['username']
35
+
36
+ # client_type
37
+ @client_type = client_type
38
+ @gen_csv_url = "#{ENV['api_url']}/v2/generate_csv_input"
39
+ @env = ENV['env']?ENV['env']:Rails.env
40
+ end
41
+
42
+ #
43
+ # login
44
+ #
45
+ # login to the navi-cloud and get the authentication token
46
+ #
47
+ def login
48
+ url = "#{@sso_web_url}/oauth/token"
49
+ provider_url = url
50
+ token = HTTParty.post(provider_url,
51
+ body: {
52
+ client_id: config["uid"], # get from sso_web application
53
+ client_secret: config["secret_key"],
54
+ grant_type: "client_credentials"
55
+ }
56
+ )['access_token']
57
+ @token = "Token: #{token}##{config['username']}"
58
+ end
59
+
60
+ def override_logger(logger)
61
+ @logger = logger
62
+ end
63
+
64
+ def set_token(token)
65
+ @token = token
66
+ end
67
+
68
+ ##
69
+ # Downloads the email content from imap-server and save it to the download_path
70
+ #
71
+ def download(message, custom_uid)
72
+
73
+ mime_type = message["mime_type"].nil? ? message.mime_type : message["mime_type"]
74
+
75
+ if ['text/plain', 'text/html', 'text/alternative'].include? mime_type
76
+
77
+ h = Hash.new
78
+ out_file = @download_path + mime_type + "/"+custom_uid
79
+ mkdir_if_not_exist(out_file)
80
+
81
+ File.open(out_file, 'w') { |file| file.write(encrypt(message["decoded"].nil? ? message.decoded : message["decoded"])) }
82
+ key = mime_type.split("/").join("_")
83
+
84
+ h[key] = out_file
85
+ return h
86
+ end
87
+ end
88
+
89
+ ##
90
+ # save data to download_path with file named by filename params.
91
+ # Input is hashmap, and it save the hashmap as yml format.
92
+ #
93
+ def save(data={}, filename, stamp)
94
+ temp_path = @download_path + "inputs/temp-#{stamp}"
95
+ filepath = @download_path + filename + ".yml"
96
+
97
+ mkdir_if_not_exist(filepath)
98
+ mkdir_if_not_exist(temp_path)
99
+ File.open(filepath, 'w') do |f|
100
+ f.puts(data.to_yaml)
101
+ end
102
+
103
+ File.open(temp_path, 'a') do |f|
104
+ f.puts(filepath)
105
+ end
106
+
107
+ return filepath
108
+ end
109
+
110
+ ##
111
+ # send bulk request to navi-ai service with list of input files downloaded from imap server.
112
+ #
113
+ def send_request(in_filenames = [], stamp, is_last: false)
114
+ unless in_filenames.empty?
115
+ filename = @download_path + "inputs/#{stamp}"
116
+
117
+ mkdir_if_not_exist(filename)
118
+
119
+ File.open(filename, 'w') do |f|
120
+ in_filenames.each { |element| f.puts(element) }
121
+ end
122
+
123
+ response = HTTPService::NaviAI.start(filepath: filename, client_type: @client_type, token: @token, email: config['username'], is_last: is_last)
124
+ return response
125
+ end
126
+ end
127
+
128
+ def mkdir_if_not_exist(filepath)
129
+ dirname = File.dirname(filepath)
130
+ unless File.directory?(dirname)
131
+ FileUtils.mkdir_p(dirname)
132
+ end
133
+ end
134
+
135
+ #
136
+ # idle_loop
137
+ #
138
+ # check for any further mail with "real-time" responsiveness.
139
+ # retrieve any mail from a folder, following specified search condition
140
+ # for any mail retrieved call a specified block
141
+ #
142
+ def idle_loop(search_condition, folder, server, username, password, callback = nil, navi_control = nil)
143
+
144
+ @logger.info "\nwaiting new mails (IDLE loop)..."
145
+
146
+ loop do
147
+ begin
148
+ @imap.select folder
149
+ @imap.idle do |resp|
150
+
151
+ # You'll get all the things from the server. For new emails (EXISTS)
152
+ if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS"
153
+
154
+ # @logger.debug resp.inspect if @debug
155
+ # Got something. Send DONE. This breaks you out of the blocking call
156
+ @logger.debug "New mail received" if @debug
157
+ @imap.idle_done
158
+ end
159
+ end
160
+
161
+ # We're out, which means there are some emails ready for us.
162
+ # Go do a search for UNSEEN and fetch them.
163
+ filenames = []
164
+ stamp = (Time.now.to_f * 1000).to_s
165
+ retrieve_emails(@imap, search_condition, folder) { |mail, i, isLast, id|
166
+ callback.call(mail, i, id, false) unless callback.nil?
167
+ filenames << process_email(mail, id, stamp)
168
+ callback.call(mail, i, id, true) unless callback.nil?
169
+ }
170
+
171
+ @logger.info "Sending Request for #{filenames.size} emails to Navi AI."
172
+ navi_control.call(true) unless navi_control.nil?
173
+ stamp = (Time.now.to_f * 1000).to_s
174
+ self.send_request(filenames, stamp, is_last: true)
175
+
176
+ # HTTPService::NaviAI.generate_csv("#{@gen_csv_url}?email=#{username}", @token)
177
+
178
+ @logger.debug "Process Completed." if @debug
179
+ navi_control.call(false) unless navi_control.nil?
180
+
181
+ rescue SignalException => e
182
+ # http://stackoverflow.com/questions/2089421/capturing-ctrl-c-in-ruby
183
+ @logger.info "Signal received at #{time_now}: #{e.class}. #{e.message}"
184
+ shutdown @imap
185
+
186
+ rescue Net::IMAP::Error => e
187
+ @logger.error "Net::IMAP::Error at #{time_now}: #{e.class}. #{e.message}"
188
+ @imap = imap_connection(server, username, password) #if e.message == 'connection closed'
189
+ @logger.info "reconnected to server: #{server}"
190
+
191
+ rescue Exception => e
192
+ @logger.error "Something went wrong at #{time_now}: #{e.class}. #{e.message}"
193
+
194
+ @imap = imap_connection(server, username, password)
195
+ @logger.info "reconnected to server: #{server}"
196
+ end
197
+ end
198
+ end
199
+
200
+ def initate_csv_generation(username)
201
+ HTTPService::NaviAI.generate_csv("#{@gen_csv_url}?email=#{username}", @token)
202
+ end
203
+
204
+ def config
205
+ YAML.load_file(CONFIG_PATH)
206
+ end
207
+
208
+ def save_aes_key_iv(key, iv)
209
+ aes_key_path = File.join(ENV['HOME'] + '/.navi/.aes.key')
210
+ iv_path = File.join(ENV['HOME'] + '/.navi/.aes.iv')
211
+
212
+ File.open(aes_key_path, "w") { |f| f.write(Base64.encode64(key)) }
213
+ File.open(iv_path, "w") { |f| f.write(Base64.encode64(iv)) }
214
+ return aes_key_path, iv_path
215
+ end
216
+
217
+ def getMessageUUIds(path)
218
+ File.directory?(path) ? (Dir.entries path).map { |i| i.split("_").first } : []
219
+ end
220
+
221
+ def update_config(data)
222
+ File.write(CONFIG_PATH, YAML.dump(data))
223
+ end
224
+
225
+ def parse_prev_failed_emails
226
+ changes = false
227
+ Dir[@download_path + 'inputs/temp*'].each do |file_name|
228
+ if File.file? file_name
229
+ HTTPService::NaviAI.start(filepath: file_name, client_type: @client_type, token: @token, email: @current_user_email )
230
+ changes = true
231
+ end
232
+ end
233
+ return changes
234
+ end
235
+
236
+ end
237
+
238
+ class GmailApi < Local
239
+ include GmailClient
240
+ end
241
+
242
+ class ImapApi < Local
243
+ include Client
244
+ end
245
+ end
@@ -0,0 +1,9 @@
1
+ require "local/navi_local_client"
2
+ require "cloud/navi_cloud_client"
3
+
4
+ ##
5
+ # This module represents the client that helps connection between navi and the imap server.
6
+ #
7
+ module NaviClient
8
+
9
+ end
@@ -0,0 +1,3 @@
1
+ module NaviClient
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'navi_client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "navi-email-sync"
8
+ spec.version = NaviClient::VERSION
9
+ spec.authors = ["Sanjib"]
10
+ spec.email = ["sanjib@whitehatengineering.com"]
11
+
12
+ spec.summary = %q{ Write a short summary, because Rubygems requires one.}
13
+ spec.description = %q{ Write a longer description or delete this line.}
14
+ spec.homepage = "http://navihq.com"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ # spec.metadata['allowed_push_host'] = "Set to 'https://rubygems.org'"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.11"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "rspec", "~> 3.0"
33
+ spec.add_runtime_dependency "httparty"
34
+ spec.add_runtime_dependency "mail"
35
+ spec.add_runtime_dependency "google-api-client", ">=0.23.0"
36
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0.5"
37
+ end
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: navi-email-sync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sanjib
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-06-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: httparty
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mail
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: google-api-client
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.23.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 0.23.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: concurrent-ruby
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 1.0.5
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.0.5
111
+ description: " Write a longer description or delete this line."
112
+ email:
113
+ - sanjib@whitehatengineering.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".travis.yml"
121
+ - CODE_OF_CONDUCT.md
122
+ - Gemfile
123
+ - LICENSE.txt
124
+ - README.md
125
+ - Rakefile
126
+ - bin/.DS_Store
127
+ - bin/console
128
+ - bin/setup
129
+ - config/navi_client.sample.yml
130
+ - lib/client.rb
131
+ - lib/cloud/navi_cloud_client.rb
132
+ - lib/gmail_client.rb
133
+ - lib/http_service/naviai.rb
134
+ - lib/local/navi_local_client.rb
135
+ - lib/navi_client.rb
136
+ - lib/navi_client/version.rb
137
+ - navi_client.gemspec
138
+ homepage: http://navihq.com
139
+ licenses:
140
+ - MIT
141
+ metadata: {}
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubyforge_project:
158
+ rubygems_version: 2.7.3
159
+ signing_key:
160
+ specification_version: 4
161
+ summary: Write a short summary, because Rubygems requires one.
162
+ test_files: []