glima 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 27d1459f172ac0f758a2ee88cd5d99cb7f8b2daa
4
- data.tar.gz: 933620cec4309b6ef67a6d845e3bd3adc6089b84
3
+ metadata.gz: ca5fd1499fa7dac5051d98341992248ebd7e22c7
4
+ data.tar.gz: 3e6ad81a9b119da5b82ad20be18af8ea66695cf2
5
5
  SHA512:
6
- metadata.gz: f28c39166d7ba2f1ec4002140e2bb4a7120be0561334b38da9d63ab7ec02490abb2172f96b5646d03c3d23d458c02765d0cd76240ce674b01556e46a21a9e85f
7
- data.tar.gz: 42c56a60f7cb35173827f1a91b50738cdddaed7db81bb796916bdf8cc5ff4335aeed1bff54b9458b257650301d4f5ee86e4623fa6a790777901bb9cac8cfc8d7
6
+ metadata.gz: d719e2648ab79c630701fc7d26c26e5f465ac454780f4954632a39df61a62da044ea5a8ea01a94ed36b1cec18c1fa31beb3fa32ed44bd0808bc94dff7409afdd
7
+ data.tar.gz: 3d3ccb2864a204932d77918fd650c81973402deec93588d0c04e8af6af895836a52986ecb9a97168e024a28f6fd01b6d5b43cf4c19e6f52f214b6ef0ad4f4da4
data/README.org CHANGED
@@ -26,6 +26,13 @@
26
26
 
27
27
  * Installation
28
28
  Glima requires Ruby 2.3 or newer.
29
+
30
+ ** Install using gem
31
+ #+BEGIN_SRC sh
32
+ $ gem install glima
33
+ #+END_SRC
34
+
35
+ ** Install in sandbox
29
36
  It uses safe navigation operator (=&.=) introduced in Ruby 2.3.
30
37
 
31
38
  #+BEGIN_SRC sh
@@ -39,15 +46,46 @@
39
46
  #+END_SRC
40
47
 
41
48
  * Setup
42
- #+BEGIN_SRC sh
43
- # Create mail cache directory
44
- $ mkdir -p ~/Mail/all
49
+ 1) Get OAuth CLIENT_ID/CLIENT_SECRET
45
50
 
46
- # Create config file
47
- $ mkdir -p ~/.config/glima
48
- $ cp examples/config_example.yml ~/.config/glima/config.yml
49
- $ vi ~/.config/glima/config.yml
51
+ Visit https://console.developers.google.com and follow the instruction.
52
+ Googling 'Creating a Google API Console project and client ID' would help.
50
53
 
51
- # Check your inbox
52
- $ glima scan +inbox
53
- #+END_SRC
54
+ 2) Create app config file interactively
55
+ #+BEGIN_SRC sh
56
+ $ glima init
57
+
58
+ Creating ~/.config/glima/config.yml ...
59
+ Get your CLIENT_ID/CLIENT_SECRET at https://console.developers.google.com
60
+ Googling 'Creating a Google API Console project and client ID' would help.
61
+ CLIENT_ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
62
+ CLIENT_SECRET: xxxxxxxxxxxxxxxxxxxxxxxx
63
+ Gmail address: xxxxxxxxxxxxxxx@gmail.com
64
+
65
+ Making config directory ~/.config/glima ...
66
+ create /Users/nom/.config/glima
67
+ Making cache directory ~/.cache/glima ...
68
+ exist Ignore /Users/nom/.cache/glima
69
+ Copying file(s) into ~/.config/glima/config.yml ...
70
+ exist Ignore ~/.config/glima
71
+ ok copy /Users/nom/.config/glima/config.yml
72
+ done.
73
+ #+END_SRC
74
+
75
+ 3) Grant OAuth access to Gmail server (browser will be invoked)
76
+ #+BEGIN_SRC sh
77
+ $ glima auth
78
+
79
+ Authenticating xxxxxxxxxxxxxxx@gmail.com...
80
+ Enter the resulting code: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
81
+ #+END_SRC
82
+ If you want to add another account, try ~glima auth --user=another_account~.
83
+
84
+ 4) Check your inbox
85
+ #+BEGIN_SRC sh
86
+ $ glima scan +inbox
87
+
88
+ 1 09/27 14:22 15ec1c9bd2c7f18d Hello....
89
+ 2 09/27 14:00 15ec1b716bbb6bdc Yeah...
90
+ :
91
+ #+END_SRC
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Usage: mail-finder mailfile.eml passwordish_mail_directory output_directory
4
+
5
+ ################################################################
6
+ # rbenv support:
7
+ # If this file is a symlink, and bound to a specific ruby
8
+ # version via rbenv (indicated by RBENV_VERSION),
9
+ # I want to resolve the symlink and re-exec
10
+ # the original executable respecting the .ruby_version
11
+ # which should indicate the right version.
12
+ #
13
+ if File.symlink?(__FILE__) and ENV["RBENV_VERSION"]
14
+ ENV["RBENV_VERSION"] = nil
15
+ shims_path = File.expand_path("shims", ENV["RBENV_ROOT"])
16
+ ENV["PATH"] = shims_path + ":" + ENV["PATH"]
17
+ exec(File.readlink(__FILE__), *ARGV)
18
+ end
19
+
20
+ gemfile = File.expand_path("../../Gemfile", __FILE__)
21
+
22
+ if File.exists?(gemfile + ".lock")
23
+ ENV["BUNDLE_GEMFILE"] = gemfile
24
+ require "bundler/setup"
25
+ end
26
+
27
+ require "rubygems"
28
+ require "date"
29
+ require "mail"
30
+ require "glima"
31
+ require "logger"
32
+
33
+ logger = ::Logger.new($stderr)
34
+ logger.formatter = proc {|severity, datetime, progname, msg| "#{msg}\n"}
35
+
36
+ Encoding.default_external="UTF-8"
37
+
38
+ def find_nearby_mail(dir, &block)
39
+ if File.directory?(dir)
40
+ Dir.glob(File.expand_path('[0-9]*', dir)) do |eml|
41
+ header = File.open(eml, "rt") {|f| f.gets("")}
42
+ if header =~ /^Date: (.*)$/
43
+ begin
44
+ yield(eml, Date.parse($1))
45
+ rescue ArgumentError
46
+ nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ mail = Glima::Resource::Mail.read(ARGV.shift)
54
+ hint = ARGV.shift
55
+ directory = ARGV.shift || "~/Downloads/attachments"
56
+
57
+ password_candidates = []
58
+
59
+ begin
60
+ password_candidates += File.open(File.expand_path("~/.config/glima/passwords.txt")).read.strip.split("\n")
61
+ rescue
62
+ end
63
+
64
+ find_nearby_mail(hint) do |eml, date|
65
+ if (mail.date - date).to_i.abs < 2
66
+ candidate = Glima::Resource::Mail.read(eml)
67
+ if candidate.from == mail.from
68
+ puts "#{date}: #{File.basename(eml)}: #{mail.from.first} #{mail.subject}"
69
+ password_candidates += candidate.find_passwordish_strings
70
+ end
71
+ end
72
+ end
73
+
74
+ # try to unlock zip attachments
75
+ unless mail.unlock_zip!(password_candidates, logger)
76
+ puts "Password unlock failed."
77
+ exit 1
78
+ end
79
+
80
+ # Write to unlocked zip file to DIRECTORY
81
+ mail.attachments.each do |attachment|
82
+ next unless attachment.filename =~ /\.zip$/i
83
+ zip_filename = File.expand_path(attachment.filename, directory)
84
+ Glima::Zip.new(attachment.body.decoded).write_to_file(zip_filename)
85
+ puts "Wrote to #{zip_filename || 'STDOUT'}."
86
+ end
data/exe/glima CHANGED
@@ -32,6 +32,12 @@ Encoding.default_external="UTF-8"
32
32
 
33
33
  class GlimaCLI < Clian::Cli
34
34
  package_name 'GLIMA'
35
+ remove_command :configuration
36
+
37
+ Signal.trap(:INT){
38
+ Thor.new.say "Interrupted quit.", :red
39
+ exit(0)
40
+ }
35
41
 
36
42
  ################################################################
37
43
  # global option
@@ -52,12 +58,13 @@ class GlimaCLI < Clian::Cli
52
58
  desc "auth", "Authenticate interactively"
53
59
 
54
60
  def auth
55
- conf = config.general
56
- authorizer = Clian::Authorizer.new(conf.client_id,
57
- conf.client_secret,
58
- Google::Apis::GmailV1::AUTH_SCOPE,
59
- conf.token_store_path)
60
- authorizer.auth_interactively(@user)
61
+ puts "Authenticating #{client.user}..."
62
+ begin
63
+ client.auth_interactively
64
+ rescue Glima::GmailClient::AuthorizationError
65
+ Thor.new.say "ERROR: authorization failed.", :red
66
+ exit(1)
67
+ end
61
68
  end
62
69
 
63
70
  ################################################################
@@ -68,6 +75,7 @@ class GlimaCLI < Clian::Cli
68
75
  method_option :zip_passwords_file, :desc => "Set additional password-list file."
69
76
 
70
77
  def dezip(gmail_id, directory)
78
+ @logger.info "Start"
71
79
  password_file = options[:password_file] || config.general.zip_passwords_file
72
80
  Glima::Command::Dezip.new(gmail_id, directory, password_file)
73
81
  end
@@ -100,8 +108,7 @@ class GlimaCLI < Clian::Cli
100
108
  expand_option :config
101
109
 
102
110
  def init
103
- config_file = options[:config] || DEFAULT_CONFIG_PATH
104
- Glima::Command::Init.new(config_file)
111
+ Glima::Command::Init.new(@config_path)
105
112
  end
106
113
 
107
114
  ################################################################
@@ -176,7 +183,7 @@ class GlimaCLI < Clian::Cli
176
183
  ################################################################
177
184
  # Command: scan
178
185
  ################################################################
179
- desc "scan +FOLDER [QUERY]", "Scan FOLDER with QUERY."
186
+ desc "scan +LABEL [QUERY]", "Scan LABEL with QUERY."
180
187
 
181
188
  method_option :format, :desc => "Set printing format", :enum => %w(mew text archive)
182
189
 
@@ -220,18 +227,41 @@ class GlimaCLI < Clian::Cli
220
227
  ################################################################
221
228
  # Command: watch
222
229
  ################################################################
223
- desc "watch [LABEL]", "Watch gmail server and xzip."
230
+ desc "watch QUEUE_LABEL MARK_LABEL", "Watch gmail server and xzip."
224
231
 
225
232
  def watch(queue_label, mark_label)
226
- queue_label = parse_label_names(queue_label).first
227
- mark_label = parse_label_names(mark_label).first
228
- Glima::Command::Watch.new(queue_label, mark_label)
233
+ default_passwords = []
234
+ password_file = options[:password_file] || config.general.zip_passwords_file
235
+ if File.exists?(File.expand_path(password_file))
236
+ default_passwords += File.open(File.expand_path(password_file)) {|f| f.read.split(/\n+/) }
237
+ end
238
+
239
+ error_count = 0
240
+ begin
241
+ ql = parse_label_names(queue_label).first
242
+ ml = parse_label_names(mark_label).first
243
+ timestamp = Time.now
244
+ Glima::Command::Watch.new(ql, ml, default_passwords)
245
+ rescue => err
246
+ @logger.info err
247
+ @logger.info err.backtrace
248
+ error_count += 1
249
+
250
+ # If Watch command lived longer than 60 seconds, it must have
251
+ # worked at least once. So, reset error_count
252
+ error_count = 0 if Time.now - timestamp > 60
253
+
254
+ wait = 2 ** [error_count, 6].min
255
+ @logger.info "Waiting retry for #{wait} seconds."
256
+ sleep wait
257
+ retry
258
+ end
229
259
  end
230
260
 
231
261
  ################################################################
232
262
  # Command: xzip
233
263
  ################################################################
234
- desc "xzip TARGET", "Transform zip-attachments in TARGET and transmit to Gmail."
264
+ desc "xzip TARGET", "Transform zip-attachments in TARGET and push back to Gmail."
235
265
 
236
266
  method_option :add_src_labels, :desc => "Add labels to source", :banner => "LABEL,..."
237
267
  method_option :add_dst_labels, :desc => "Add labels to destination", :banner => "LABEL,..."
@@ -239,7 +269,14 @@ class GlimaCLI < Clian::Cli
239
269
  method_option :del_dst_labels, :desc => "Del labels from destination", :banner => "LABEL,..."
240
270
 
241
271
  def xzip(target)
242
- Glima::Command::Xzip.new(target,
272
+ default_passwords = []
273
+
274
+ password_file = options[:password_file] || config.general.zip_passwords_file
275
+ if File.exists?(password_file)
276
+ default_passwords += File.open(password_file) {|f| f.read.split(/\n+/) }
277
+ end
278
+
279
+ Glima::Command::Xzip.new(target, default_passwords,
243
280
  add_src_labels: parse_label_names(options[:add_src_labels]),
244
281
  del_src_labels: parse_label_names(options[:del_src_labels]),
245
282
  add_dst_labels: parse_label_names(options[:add_dst_labels]),
@@ -265,47 +302,49 @@ class GlimaCLI < Clian::Cli
265
302
  end
266
303
  end
267
304
 
268
- attr_reader :builder, :config, :user, :context
305
+ attr_reader :builder, :config, :user, :context, :client
269
306
 
270
307
  def setup_global_options(command, *args)
271
308
  exit_on_error do
272
- @config = Glima::Config.create_from_file(options[:config] || DEFAULT_CONFIG_PATH) unless command.name == "init"
273
- @datastore = Glima::DataStore.new(File.expand_path("~/Mail"))
274
- @user = options[:user] || config.general.default_user
275
- @context = Glima::Context.new(File.expand_path(DEFAULT_CONFIG_HOME))
276
-
277
- logger = ::Logger.new($stderr)
278
- logger.formatter = proc {|severity, datetime, progname, msg| "#{msg}\n"}
279
-
280
- Glima::Command.client = client unless command.name == "auth"
281
- Glima::Command.logger = logger
282
- # Glima::GmailClient.logger = logger
283
-
284
309
  if options[:profile]
285
310
  require 'profiler'
286
311
  Profiler__.start_profile
287
312
  end
313
+
288
314
  if options[:debug]
289
315
  require "pp"
290
316
  $GLIMA_DEBUG = true
291
317
  $GLIMA_DEBUG_FOR_DEVELOPER = true if ENV["GLIMA_DEBUG_FOR_DEVELOPER"]
292
318
  end
293
- end
294
- end
295
319
 
296
- def client
297
- begin
298
- @client ||= Glima::GmailClient.new(config.general, @user, @datastore)
299
- rescue Glima::GmailClient::AuthorizationError
300
- STDERR.print "Authorization Error: try ``glima auth'' command.\n"
301
- exit(1)
302
- end
303
- end
304
- end
320
+ @logger = ::Logger.new($stderr)
321
+ @logger.formatter = proc {|severity, datetime, progname, msg| "#{datetime} #{msg}\n"}
322
+ Glima::Command.logger = @logger
323
+
324
+ @config_path = options[:config] || DEFAULT_CONFIG_PATH
305
325
 
306
- def teardown
307
- if options[:profile]
308
- Profiler__.print_profile($stdout)
326
+ return true if ["init", "help"].member?(command.name)
327
+
328
+ @config = Glima::Config.create_from_file(@config_path)
329
+ @datastore = Glima::DataStore.new(File.expand_path(config.general.cache_directory))
330
+ @user = options[:user] || config.general.default_user
331
+ @context = Glima::Context.new(File.expand_path(DEFAULT_CONFIG_HOME))
332
+ @client = Glima::GmailClient.new(config.general.client_id,
333
+ config.general.client_secret,
334
+ File.expand_path("token_store.yml", File.dirname(@config_path)),
335
+ @user, @datastore, @logger)
336
+
337
+ Glima::Command.client = @client
338
+
339
+ unless ["auth"].member?(command.name)
340
+ begin
341
+ @client.auth
342
+ rescue Glima::GmailClient::AuthorizationError
343
+ Thor.new.say "ERROR: access token expired? try: glima auth --user=#{@user}", :red
344
+ exit(1)
345
+ end
346
+ end
347
+ end
309
348
  end
310
349
  end
311
350
 
@@ -15,13 +15,16 @@ module Glima
15
15
 
16
16
  # get password candidates from config file
17
17
  password_candidates = []
18
+ password_file = File.expand_path(password_file)
18
19
  if File.exists?(password_file)
19
20
  password_candidates += File.open(password_file) {|f| f.read.split(/\n+/) }
20
21
  end
21
22
 
22
23
  # gather password candidates from nearby mails
24
+ index = 0
23
25
  client.nearby_mails(mail) do |nm|
24
- logger.info "Passwordish mail: " + nm.format_summary
26
+ index += 1
27
+ logger.info "Passwordish mail(#{index}): " + nm.format_summary
25
28
  password_candidates += nm.find_passwordish_strings
26
29
  end
27
30
 
@@ -22,14 +22,19 @@ module Glima
22
22
  config[:client_id] = @shell.ask "CLIENT_ID:"
23
23
  config[:client_secret] = @shell.ask "CLIENT_SECRET:"
24
24
  config[:default_user] = @shell.ask "Gmail address:"
25
- config[:token_store_path] = File.expand_path("token_store.yml", config_dir)
25
+ config[:cache_directory] = "~/.cache/glima"
26
26
 
27
- # mkdir
27
+ # mkdir config_dir
28
28
  unless Dir.exist?(File.expand_path(config_dir))
29
29
  say "Making config directory #{config_dir} ..."
30
30
  mkdir_p(File.expand_path(config_dir))
31
31
  end
32
32
 
33
+ # mkdir cache_dir
34
+ cache_dir = config[:cache_directory]
35
+ say "Making cache directory #{cache_dir} ..."
36
+ mkdir_p(File.expand_path(cache_dir))
37
+
33
38
  # make config file from tamplate
34
39
  say "Copying file(s) into #{config_file} ..."
35
40
  src = File.expand_path("config.yml.erb", TEMPLATE_DIR)
@@ -79,15 +84,15 @@ module Glima
79
84
  end
80
85
 
81
86
  def mkdir_p(path)
82
- path = File.expand_path(path)
87
+ abspath = File.expand_path(path)
83
88
 
84
- if File.directory?(path)
89
+ if File.directory?(abspath)
85
90
  say_status "exist", "Ignore #{path}", :yellow
86
91
  return
87
92
  end
88
93
 
89
94
  begin
90
- FileUtils.mkdir_p(path)
95
+ FileUtils.mkdir_p(abspath)
91
96
  say_status "create", "#{path}", :green
92
97
  rescue StandardError => e
93
98
  say_status "failed", "#{e.message.split(' @').first} #{path}", :red
@@ -2,42 +2,49 @@ module Glima
2
2
  module Command
3
3
  class Watch < Base
4
4
 
5
- def initialize(queue_label = nil, mark_label = nil)
5
+ def initialize(queue_label = nil, mark_label = nil, default_passwords = [])
6
+ # If xzip is successful,
7
+ # remove queue_label from the original message, also
8
+ # add mark_label to the xzipped message.
9
+ #
10
+ add_labels, del_labels = [], []
11
+ add_labels << mark_label if mark_label
12
+ del_labels << queue_label if queue_label
13
+
14
+ # if queue_label is set, xzip process scan search zip-attached
15
+ # messages with queue_label, without mark_label
16
+ #
17
+ if queue_label
18
+ target = "label:#{queue_label.name}"
19
+ target += " -label:#{mark_label.name}" if mark_label
20
+ end
21
+
22
+ # Cleanup queue before watching imap events.
23
+ logger.info "xzip cleanup queue before watching imap events #{target}."
24
+ Glima::Command::Xzip.new(target, default_passwords,
25
+ add_dst_labels: add_labels,
26
+ del_dst_labels: del_labels,
27
+ del_src_labels: del_labels)
6
28
 
7
29
  # Watch "[Gmail]/All Mail" by IMAP idle
8
- client.watch(nil) do |ev|
9
- next unless ev.type == :added
10
-
11
- # Scan messages in queue_label or new message itself.
12
- #
13
- # If Xzip process is successful, remove queue_label
14
- # from the source message.
15
- #
16
- if queue_label
17
- target = "label:#{queue_label.name}"
18
- target += " -label:#{mark_label.name}" if mark_label
19
- del_labels = [queue_label]
20
- else
21
- target = ev.message.id
22
- del_labels = []
23
- end
24
-
25
- # Also, mark_label will be added to the xzipped message.
26
- # It is for avoidance of infinite loop.
27
- #
28
- if mark_label
29
- add_labels = [mark_label]
30
- else
31
- add_labels = []
32
- end
33
-
34
- logger.info "Xzip #{target}"
35
-
36
- Glima::Command::Xzip.new(client, logger, target,
30
+
31
+ timestamp = Time.now
32
+
33
+ logger.info "[#{self.class}#initialize] Entering GmailClient#watch"
34
+ client.watch do |ev|
35
+ # avoid burst events
36
+ next if Time.now - timestamp < 3
37
+
38
+ logger.info "[#{self.class}#initialize] xzip #{target} in event loop."
39
+
40
+ target ||= ev.message.id
41
+ Glima::Command::Xzip.new(target, default_passwords,
37
42
  add_dst_labels: add_labels,
38
43
  del_dst_labels: del_labels,
39
44
  del_src_labels: del_labels)
45
+ timestamp = Time.now
40
46
  end
47
+ logger.info "[#{self.class}#initialize] Done (not reached)"
41
48
  end
42
49
 
43
50
  end # class Watch
@@ -1,7 +1,7 @@
1
1
  module Glima
2
2
  module Command
3
3
  class Xzip < Base
4
- def initialize(target,
4
+ def initialize(target, default_passwords,
5
5
  add_src_labels: [],
6
6
  del_src_labels: [],
7
7
  add_dst_labels: [],
@@ -20,15 +20,22 @@ module Glima
20
20
 
21
21
  ids.each do |message_id|
22
22
  # get target mail
23
+ logger.info "xzip start #{message_id}"
24
+
23
25
  mail = client.get_user_smart_message(message_id) do |m, err|
24
26
  if err
25
- puts "Error: #{err}"
27
+ logger.error "Error: #{err}"
26
28
  next
27
29
  end
28
30
  end
29
31
 
32
+ unless mail.attachments.map(&:filename).any? {|filename| filename =~ /\.zip$/i}
33
+ logger.info "xzip skip #{message_id} - has no zip attachments"
34
+ next
35
+ end
36
+
30
37
  # find password candidates from nearby mails
31
- password_candidates = []
38
+ password_candidates = default_passwords || []
32
39
  client.nearby_mails(mail) do |nm|
33
40
  logger.info "Passwordish mail: " + nm.format_summary
34
41
  password_candidates += nm.find_passwordish_strings
@@ -36,13 +43,13 @@ module Glima
36
43
 
37
44
  # try to unlock zip attachments
38
45
  unless mail.unlock_zip!(password_candidates, logger)
39
- puts "Password unlock failed."
46
+ logger.info "Password unlock failed."
40
47
  next
41
48
  end
42
49
 
43
50
  # push back unlocked mail to server
44
51
  unless push_mail(mail, "dateHeader", add_dst_label_ids, del_dst_label_ids)
45
- puts "Push mail failed."
52
+ logger.info "Push mail failed."
46
53
  next
47
54
  end
48
55
 
@@ -59,9 +66,9 @@ module Glima
59
66
 
60
67
  client.modify_message('me', message_id, req) do |res,err|
61
68
  if res
62
- puts "Update #{message_id} successfully."
69
+ logger.info "[#{self.class}#initialize] Update #{message_id} successfully."
63
70
  else
64
- puts "Error: #{err}"
71
+ logger.info "[#{self.class}#initialize] Error: #{err}"
65
72
  end
66
73
  end
67
74
  end
@@ -89,10 +96,10 @@ module Glima
89
96
  internal_date_source: date_source,
90
97
  upload_source: StringIO.new(mail.to_s)) do |msg, err|
91
98
  if msg
92
- puts "pushed to: #{msg.id}"
99
+ logger.info "[#{self.class}#push_mail] pushed to: #{msg.id}"
93
100
  return true
94
101
  else
95
- STDERR.puts "Error: #{err}"
102
+ logger.info "[#{self.class}#push_mail] Error: #{err}"
96
103
  return false
97
104
  end
98
105
  end
@@ -4,8 +4,7 @@ module Glima
4
4
  class General < Clian::Config::Element
5
5
  define_syntax :client_id => String,
6
6
  :client_secret => String,
7
- :token_store_path => String,
8
- :context_store => String,
7
+ :cache_directory => String,
9
8
  :zip_passwords_file => String,
10
9
  :default_user => String
11
10
  end # class General
@@ -8,7 +8,12 @@ module Glima
8
8
  unless basedir and File.directory?(File.expand_path(basedir.to_s))
9
9
  raise Glima::ConfigurationError, "datastore directory '#{basedir}' not found"
10
10
  end
11
+
11
12
  @basedir = Pathname.new(File.expand_path(basedir))
13
+
14
+ unless Dir.exist?(dir = File.expand_path("all", @basedir))
15
+ FileUtils.mkdir_p(dir)
16
+ end
12
17
  end
13
18
 
14
19
  def update(message)
@@ -11,14 +11,6 @@ module Glima
11
11
  class GmailClient
12
12
  class AuthorizationError < StandardError ; end
13
13
 
14
- def self.logger
15
- Google::Apis.logger
16
- end
17
-
18
- def self.logger=(logger)
19
- Google::Apis.logger = logger
20
- end
21
-
22
14
  extend Forwardable
23
15
 
24
16
  def_delegators :@client,
@@ -43,6 +35,7 @@ module Glima
43
35
  # Users getProfile
44
36
  :get_user_profile
45
37
 
38
+ attr_reader :user
46
39
 
47
40
  # Find nearby messages from pivot_message
48
41
  # `Nearby' message:
@@ -92,40 +85,53 @@ module Glima
92
85
  }.map(&:addr).map(&:ip_address).length > 0
93
86
  end
94
87
 
95
- def initialize(config, user, datastore)
96
- authorizer = Clian::Authorizer.new(config.client_id,
97
- config.client_secret,
98
- Google::Apis::GmailV1::AUTH_SCOPE,
99
- config.token_store_path)
100
-
101
- unless credentials = authorizer.credentials(user)
102
- raise AuthorizationError.new
103
- end
104
-
88
+ def initialize(client_id, client_secret, token_store_path, user, datastore, logger = nil)
89
+ @client_id = client_id
90
+ @client_secret = client_secret
91
+ @token_store_path = token_store_path
92
+ @user = user
105
93
  @datastore = datastore
106
94
  @client = Google::Apis::GmailV1::GmailService.new
107
95
  @client.client_options.application_name = 'glima'
108
- @client.authorization = credentials
109
- @client.authorization.username = user # for IMAP
96
+ if logger
97
+ @logger = logger
98
+ else
99
+ # quiet
100
+ @logger = ::Logger.new($stderr)
101
+ @logger.formatter = proc {|severity, datetime, progname, msg| ""}
102
+ end
103
+ end
110
104
 
111
- return @client
105
+ def auth_interactively
106
+ credentials = begin
107
+ authorizer.auth_interactively(@user)
108
+ rescue
109
+ raise AuthorizationError.new
110
+ end
111
+ @client.authorization = credentials
112
+ @client.authorization
113
+ @client.authorization.username = @user # for IMAP
112
114
  end
113
115
 
114
- # label == nil means "[Gmail]/All Mail"
115
- def wait(label = nil)
116
- @imap ||= Glima::ImapWatch.new("imap.gmail.com", @client.authorization)
117
- @imap.wait(label&.name)
116
+ def auth
117
+ unless credentials = authorizer.credentials(@user)
118
+ raise AuthorizationError.new
119
+ end
120
+ @client.authorization = credentials
121
+ @client.authorization.username = @user # for IMAP
118
122
  end
119
123
 
120
124
  def watch(label = nil, &block)
121
125
  loop do
122
- puts "tick"
126
+ @logger.info "[#{self.class}#watch] loop tick"
123
127
 
124
128
  curr_hid = get_user_profile(me).history_id.to_i
125
129
  last_hid ||= curr_hid
126
130
 
127
- # FIXME: if server is changed at this point, we will miss the event.
128
- wait(label) if last_hid == curr_hid
131
+ # If server is changed at this point, we will miss the events.
132
+ # so, we have to set the timeout and update history record.
133
+
134
+ wait(label, 60) if last_hid == curr_hid
129
135
 
130
136
  each_events(since: last_hid) do |ev|
131
137
  yield ev
@@ -214,6 +220,15 @@ module Glima
214
220
 
215
221
  private
216
222
 
223
+ def authorizer
224
+ @authorizer ||= Clian::Authorizer.new(
225
+ @client_id,
226
+ @client_secret,
227
+ Google::Apis::GmailV1::AUTH_SCOPE,
228
+ @token_store_path
229
+ )
230
+ end
231
+
217
232
  def me
218
233
  'me'
219
234
  end
@@ -236,5 +251,26 @@ module Glima
236
251
  end
237
252
  end
238
253
 
254
+ # label == nil means "[Gmail]/All Mail"
255
+ def wait(label = nil, timeout_sec = 60)
256
+ @logger.info "[#{self.class}#wait] Enter"
257
+
258
+ if @imap.nil? || @imap.disconnected?
259
+ @imap = Glima::ImapWatch.new("imap.gmail.com", @client.authorization, @logger)
260
+
261
+ @logger.debug "[#{self.class}#wait] create new IMAPWatch #{@imap}"
262
+ else
263
+ @logger.debug "[#{self.class}#wait] use existing IMAPWatch #{@imap}"
264
+ end
265
+
266
+ begin
267
+ @imap.wait(label&.name, timeout_sec)
268
+ rescue
269
+ @imap = nil
270
+ @logger.info "[#{self.class}#wait] imap connection error. abandon current imap connection."
271
+ end
272
+ @logger.info "[#{self.class}#wait] Exit"
273
+ end
274
+
239
275
  end # class GmailClient
240
276
  end # module Glima
@@ -3,6 +3,10 @@ require "net/imap"
3
3
  require "pp"
4
4
 
5
5
  module Glima
6
+ #
7
+ # IMAP client with IMAP extentions provided by Gmail
8
+ # https://developers.google.com/gmail/imap/imap-extensions
9
+ #
6
10
  class IMAP < Net::IMAP
7
11
  def initialize(host, port_or_options = {},
8
12
  usessl = false, certs = nil, verify = true)
@@ -86,33 +90,16 @@ module Glima
86
90
  def_delegators :@imap,
87
91
  :fetch,
88
92
  :select,
89
- :disconnect
93
+ :disconnected?
90
94
 
91
- def initialize(imap_server, authorization)
95
+ def initialize(imap_server, authorization, logger)
96
+ @imap_server, @authorization, @logger = imap_server, authorization, logger
92
97
  connect(imap_server, authorization)
93
98
  end
94
99
 
95
- def watch(folder, &block)
96
- @imap.select(Net::IMAP.encode_utf7(folder))
100
+ def wait(folder = nil, timeout_sec = 60)
101
+ logger.info "[#{self.class}#wait] Enter"
97
102
 
98
- @thread = Thread.new do
99
- while true
100
- begin
101
- @imap.idle do |resp|
102
- yield resp if resp.name == "EXISTS"
103
- end
104
- rescue Net::IMAP::Error => e
105
- if e.inspect.include? "connection closed"
106
- connect
107
- else
108
- raise
109
- end
110
- end
111
- end
112
- end
113
- end
114
-
115
- def wait(folder = nil)
116
103
  if folder
117
104
  folder = Net::IMAP.encode_utf7(folder)
118
105
  else
@@ -122,84 +109,46 @@ module Glima
122
109
 
123
110
  @imap.select(folder)
124
111
  begin
125
- @imap.idle do |resp|
126
- puts "#{resp.name}"
127
- @imap.idle_done # if resp.name == "EXISTS"
128
- end
129
- rescue Net::IMAP::Error => e
130
- if e.inspect.include? "connection closed"
131
- connect
132
- else
133
- raise
134
- end
135
- end
136
- end
137
-
138
- # Ruby IMAP IDLE concurrency - how to tackle? - Stack Overflow
139
- # http://stackoverflow.com/questions/5604480/ruby-imap-idle-concurrency-how-to-tackle
140
- # How bad is IMAP IDLE? Joshua Tauberer's Archived Blog
141
- # https://joshdata.wordpress.com/2014/08/09/how-bad-is-imap-idle/
142
- #
143
- def watch_and_fetch(folder, &block)
144
- @imap.select(folder)
145
- num = @imap.responses["EXISTS"].last
146
-
147
- last_uid = @imap.fetch(num, "UID").first.attr['UID']
148
- puts "Last_UID: #{last_uid}"
149
-
150
- while true
151
- id = waitfor()
152
-
153
- puts "*********** GET EXISTS: #{id}"
154
- mails = @imap.uid_fetch(last_uid..-1, %w(UID X-GM-MSGID X-GM-THRID X-GM-LABELS))
155
-
156
- mails.each do |mail|
157
- next if mail.attr['UID'] <= last_uid
158
- resp = yield mail
159
- return unless resp
112
+ th = Thread.new(@imap, timeout_sec) do |imap, timeout|
113
+ begin
114
+ sleep timeout
115
+ ensure
116
+ imap.idle_done
117
+ end
160
118
  end
161
119
 
162
- last_uid = mails.last.attr['UID']
163
- end
164
- end
165
-
166
- private
167
-
168
- def waitfor
169
- id = -1
170
- begin
171
120
  @imap.idle do |resp|
172
- pp resp
173
- if resp.name == "EXISTS"
174
- @imap.idle_done
175
- id = resp.data.to_i
176
- else
177
- # pp resp
178
- end
121
+ logger.info "[#{self.class}#wait] got event #{resp.name} in IMAP IDLE"
122
+ th.terminate
179
123
  end
180
124
  rescue Net::IMAP::Error => e
181
125
  if e.inspect.include? "connection closed"
182
- connect
126
+ reconnect
183
127
  else
184
128
  raise
185
129
  end
186
130
  end
187
- return id
188
- end
131
+ logger.info "[#{self.class}#wait] Exit"
132
+ end # def wait
133
+
134
+ private
189
135
 
190
136
  def connect(imap_server, authorization)
137
+ logger.info "[#{self.class}#connect] Enter"
191
138
  retry_count = 0
192
139
 
193
140
  begin
194
- username = authorization.username
195
- access_token = authorization.access_token
196
-
197
141
  @imap = Glima::IMAP.new(imap_server, 993, true)
198
- @imap.authenticate('XOAUTH2', username, access_token)
199
- puts "connected to imap server #{imap_server}."
142
+ @imap.authenticate('XOAUTH2',
143
+ authorization.username,
144
+ authorization.access_token)
145
+ logger.info "[#{self.class}#connect] connected"
200
146
 
201
147
  rescue Net::IMAP::NoResponseError => e
148
+ logger.info "[#{self.class}#connect] rescue Net::IMAP::NoResponseError => e"
149
+
202
150
  if e.inspect.include? "Invalid credentials" && retry_count < 2
151
+ logger.info "[#{self.class}#connect] Refreshing access token for #{imap_server}."
203
152
  authorization.refresh!
204
153
  retry_count += 1
205
154
  retry
@@ -207,13 +156,18 @@ module Glima
207
156
  raise
208
157
  end
209
158
  end
159
+ logger.info "[#{self.class}#connect] Exit"
160
+ end
161
+
162
+ def reconnect
163
+ logger.info "[#{self.class}#reconnect] Enter"
164
+ connect(@imap_server, @authorization)
165
+ logger.info "[#{self.class}#reconnect] Exit"
166
+ end
167
+
168
+ def logger
169
+ @logger
210
170
  end
211
171
 
212
- # def connect(imap_server, user_name, access_token)
213
- # @imap = Glima::IMAP.new(imap_server, 993, true)
214
- # # @imap.class.debug = true
215
- # @imap.authenticate('XOAUTH2', user_name, access_token)
216
- # puts "connected to imap server #{imap_server}."
217
- # end
218
172
  end # class ImapWatch
219
173
  end # module Glima
@@ -79,7 +79,7 @@ module Glima
79
79
  end
80
80
 
81
81
  def format_summary(count = nil)
82
- date = Time.at(internal_date.to_i/1000).strftime("%m/%d %H:%M")
82
+ date = Time.at(internal_date.to_i/1000).strftime("%Y/%m/%d %H:%M")
83
83
  count = if count then ("%4d " % count) else "" end
84
84
  return "#{count}#{date} #{id} #{CGI.unescapeHTML(snippet)[0..30]}"
85
85
  end
@@ -3,5 +3,6 @@ GENERAL:
3
3
  ################################################################
4
4
  CLIENT_ID: "<%= config[:client_id] %>"
5
5
  CLIENT_SECRET: "<%= config[:client_secret] %>"
6
- TOKEN_STORE_PATH: "<%= config[:token_store_path] %>"
7
6
  DEFAULT_USER: "<%= config[:default_user] %>"
7
+ CACHE_DIRECTORY: "<%= config[:cache_directory] %>"
8
+ # ZIP_PASSWORDS_FILE: "~/.config/glima/passwords.txt"
@@ -1,3 +1,3 @@
1
1
  module Glima
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
@@ -38,8 +38,8 @@ module Glima
38
38
  def unlock_password!(password_candidates, logger = nil)
39
39
  list = sort_by_password_strength(password_candidates.uniq).unshift("")
40
40
 
41
- list.each do |password|
42
- msg = "Try password:'#{password}' (#{password_strength(password)})..."
41
+ list.each_with_index do |password, i|
42
+ msg = "Try password(#{i}):'#{password}' (#{password_strength(password)})..."
43
43
 
44
44
  if correct_password?(password)
45
45
  logger.info(msg + " OK.") if logger
@@ -100,7 +100,7 @@ module Glima
100
100
  password = password.to_s
101
101
  score = Math.log2(password.length + 1)
102
102
 
103
- password.scan(/[a-z]+|[A-Z]+|\d+|[!"#$%&'()*+,-.\/:;<=>?@\[\\\]^_`{|}~]+/) do |s|
103
+ password.scan(/[A-Z]?[a-z]+|[A-Z]+|[-+\d]+|[!"#$%&'()*+,-.\/:;<=>?@\[\\\]^_`{|}~]+/) do |s|
104
104
  score += 1.0
105
105
  end
106
106
  return score
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glima
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoshinari Nomura
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-09-26 00:00:00.000000000 Z
11
+ date: 2017-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -112,6 +112,7 @@ description: Gmail CLI client
112
112
  email:
113
113
  - nom@quickhack.net
114
114
  executables:
115
+ - axezip
115
116
  - glima
116
117
  extensions: []
117
118
  extra_rdoc_files: []
@@ -125,7 +126,7 @@ files:
125
126
  - Rakefile
126
127
  - bin/console
127
128
  - bin/setup
128
- - examples/config_example.yml
129
+ - exe/axezip
129
130
  - exe/glima
130
131
  - glima.gemspec
131
132
  - lib/glima.rb
@@ -1,8 +0,0 @@
1
- ################################################################
2
- GENERAL:
3
- ################################################################
4
- CLIENT_ID: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
5
- CLIENT_SECRET: "yyyyyyyyyyyyyyyyyyyyyyyy"
6
- TOKEN_STORE: "/Users/nom/.config/glima/token_store.yml"
7
- CONTEXT_STORE_PATH: "/Users/nom/.config/glima/context.yml"
8
- DEFAULT_USER: "????????@gmail.com"