glima 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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"