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 +4 -4
- data/README.org +48 -10
- data/exe/axezip +86 -0
- data/exe/glima +81 -42
- data/lib/glima/command/dezip.rb +4 -1
- data/lib/glima/command/init.rb +10 -5
- data/lib/glima/command/watch.rb +37 -30
- data/lib/glima/command/xzip.rb +16 -9
- data/lib/glima/config.rb +1 -2
- data/lib/glima/datastore.rb +5 -0
- data/lib/glima/gmail_client.rb +64 -28
- data/lib/glima/imap.rb +41 -87
- data/lib/glima/resource/mail.rb +1 -1
- data/lib/glima/templates/config.yml.erb +2 -1
- data/lib/glima/version.rb +1 -1
- data/lib/glima/zip.rb +3 -3
- metadata +4 -3
- data/examples/config_example.yml +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca5fd1499fa7dac5051d98341992248ebd7e22c7
|
4
|
+
data.tar.gz: 3e6ad81a9b119da5b82ad20be18af8ea66695cf2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
43
|
-
# Create mail cache directory
|
44
|
-
$ mkdir -p ~/Mail/all
|
49
|
+
1) Get OAuth CLIENT_ID/CLIENT_SECRET
|
45
50
|
|
46
|
-
|
47
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
data/exe/axezip
ADDED
@@ -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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
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 +
|
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
|
230
|
+
desc "watch QUEUE_LABEL MARK_LABEL", "Watch gmail server and xzip."
|
224
231
|
|
225
232
|
def watch(queue_label, mark_label)
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
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
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|
-
|
307
|
-
|
308
|
-
|
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
|
|
data/lib/glima/command/dezip.rb
CHANGED
@@ -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
|
-
|
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
|
|
data/lib/glima/command/init.rb
CHANGED
@@ -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[:
|
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
|
-
|
87
|
+
abspath = File.expand_path(path)
|
83
88
|
|
84
|
-
if File.directory?(
|
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(
|
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
|
data/lib/glima/command/watch.rb
CHANGED
@@ -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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
data/lib/glima/command/xzip.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
69
|
+
logger.info "[#{self.class}#initialize] Update #{message_id} successfully."
|
63
70
|
else
|
64
|
-
|
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
|
-
|
99
|
+
logger.info "[#{self.class}#push_mail] pushed to: #{msg.id}"
|
93
100
|
return true
|
94
101
|
else
|
95
|
-
|
102
|
+
logger.info "[#{self.class}#push_mail] Error: #{err}"
|
96
103
|
return false
|
97
104
|
end
|
98
105
|
end
|
data/lib/glima/config.rb
CHANGED
@@ -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
|
-
:
|
8
|
-
:context_store => String,
|
7
|
+
:cache_directory => String,
|
9
8
|
:zip_passwords_file => String,
|
10
9
|
:default_user => String
|
11
10
|
end # class General
|
data/lib/glima/datastore.rb
CHANGED
@@ -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)
|
data/lib/glima/gmail_client.rb
CHANGED
@@ -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(
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
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
|
-
#
|
128
|
-
|
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
|
data/lib/glima/imap.rb
CHANGED
@@ -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
|
-
:
|
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
|
96
|
-
|
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
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
173
|
-
|
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
|
-
|
126
|
+
reconnect
|
183
127
|
else
|
184
128
|
raise
|
185
129
|
end
|
186
130
|
end
|
187
|
-
|
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',
|
199
|
-
|
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
|
data/lib/glima/resource/mail.rb
CHANGED
@@ -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"
|
data/lib/glima/version.rb
CHANGED
data/lib/glima/zip.rb
CHANGED
@@ -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.
|
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]
|
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.
|
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-
|
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
|
-
-
|
129
|
+
- exe/axezip
|
129
130
|
- exe/glima
|
130
131
|
- glima.gemspec
|
131
132
|
- lib/glima.rb
|
data/examples/config_example.yml
DELETED
@@ -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"
|