glima 0.2.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 +7 -0
- data/.gitignore +52 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.org +53 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/config_example.yml +8 -0
- data/exe/glima +287 -0
- data/glima.gemspec +39 -0
- data/lib/glima.rb +18 -0
- data/lib/glima/cli.rb +151 -0
- data/lib/glima/command.rb +35 -0
- data/lib/glima/command/base.rb +23 -0
- data/lib/glima/command/dezip.rb +45 -0
- data/lib/glima/command/guess.rb +30 -0
- data/lib/glima/command/label.rb +29 -0
- data/lib/glima/command/labels.rb +63 -0
- data/lib/glima/command/profile.rb +15 -0
- data/lib/glima/command/push.rb +26 -0
- data/lib/glima/command/relabel.rb +65 -0
- data/lib/glima/command/scan.rb +32 -0
- data/lib/glima/command/trash.rb +22 -0
- data/lib/glima/command/watch.rb +45 -0
- data/lib/glima/command/xzip.rb +103 -0
- data/lib/glima/config.rb +205 -0
- data/lib/glima/context.rb +32 -0
- data/lib/glima/datastore.rb +70 -0
- data/lib/glima/gmail_client.rb +270 -0
- data/lib/glima/imap.rb +219 -0
- data/lib/glima/query_parameter.rb +30 -0
- data/lib/glima/resource.rb +31 -0
- data/lib/glima/resource/history.rb +94 -0
- data/lib/glima/resource/label.rb +22 -0
- data/lib/glima/resource/mail.rb +155 -0
- data/lib/glima/resource/message.rb +74 -0
- data/lib/glima/version.rb +3 -0
- data/lib/glima/zip.rb +156 -0
- data/spec/glima_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- metadata +213 -0
data/glima.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
git = File.expand_path('../.git', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'glima/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "glima"
|
9
|
+
spec.version = Glima::VERSION
|
10
|
+
spec.authors = ["Yoshinari Nomura"]
|
11
|
+
spec.email = ["nom@quickhack.net"]
|
12
|
+
spec.summary = %q{Gmail CLI client}
|
13
|
+
spec.description = %q{Gmail CLI client}
|
14
|
+
spec.homepage = "https://github.com/yoshinari-nomura/glima"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = if Dir.exist?(git)
|
18
|
+
`git ls-files -z`.split("\x0")
|
19
|
+
else
|
20
|
+
Dir['**/*']
|
21
|
+
end
|
22
|
+
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.required_ruby_version = ">= 2.3.0"
|
28
|
+
|
29
|
+
spec.add_runtime_dependency "thor", ">= 0.19.1"
|
30
|
+
spec.add_runtime_dependency "google-api-client", ">0.9"
|
31
|
+
spec.add_runtime_dependency "googleauth"
|
32
|
+
spec.add_runtime_dependency "launchy"
|
33
|
+
spec.add_runtime_dependency "mail"
|
34
|
+
spec.add_runtime_dependency "rubyzip"
|
35
|
+
|
36
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
37
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
38
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
39
|
+
end
|
data/lib/glima.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Glima
|
2
|
+
# Your code goes here...
|
3
|
+
class ConfigurationError < StandardError ; end
|
4
|
+
|
5
|
+
dir = File.dirname(__FILE__) + "/glima"
|
6
|
+
|
7
|
+
autoload :Cli, "#{dir}/cli.rb"
|
8
|
+
autoload :Command, "#{dir}/command.rb"
|
9
|
+
autoload :Config, "#{dir}/config.rb"
|
10
|
+
autoload :Context, "#{dir}/context.rb"
|
11
|
+
autoload :DataStore, "#{dir}/datastore.rb"
|
12
|
+
autoload :GmailClient, "#{dir}/gmail_client.rb"
|
13
|
+
autoload :ImapWatch, "#{dir}/imap.rb"
|
14
|
+
autoload :QueryParameter, "#{dir}/query_parameter.rb"
|
15
|
+
autoload :Resource, "#{dir}/resource.rb"
|
16
|
+
autoload :Zip, "#{dir}/zip.rb"
|
17
|
+
autoload :VERSION, "#{dir}/version.rb"
|
18
|
+
end
|
data/lib/glima/cli.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
module Glima
|
4
|
+
class Cli < Thor
|
5
|
+
################################################################
|
6
|
+
# config files
|
7
|
+
|
8
|
+
CONFIG_HOME = File.join((ENV["XDG_CONFIG_HOME"] || "~/.config"), basename)
|
9
|
+
CONFIG_FILE = "config.yml"
|
10
|
+
CONFIG_PATH = File.join(CONFIG_HOME, CONFIG_FILE)
|
11
|
+
|
12
|
+
def self.config_home ; CONFIG_HOME; end
|
13
|
+
def self.config_path ; CONFIG_PATH; end
|
14
|
+
|
15
|
+
################################################################
|
16
|
+
# register preset options
|
17
|
+
|
18
|
+
def self.named_option(name, options)
|
19
|
+
@named_options ||= {}
|
20
|
+
@named_options[name] = options
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.expand_option(*names)
|
24
|
+
expand_named_option(:method, *names)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.expand_class_option(*names)
|
28
|
+
expand_named_option(:class, *names)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.expand_named_option(type, *names)
|
32
|
+
names.each do |name|
|
33
|
+
options = @named_options[name]
|
34
|
+
if type == :class
|
35
|
+
class_option name, options
|
36
|
+
else
|
37
|
+
method_option name, options
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
private_class_method :expand_named_option
|
42
|
+
|
43
|
+
named_option :debug, :desc => "Set debug flag", :type => :boolean
|
44
|
+
named_option :profile, :desc => "Set profiler flag", :type => :boolean
|
45
|
+
named_option :config, :desc => "Set config path (default: #{CONFIG_PATH})", :banner => "FILE"
|
46
|
+
named_option :dry_run, :desc => "Perform a trial run with no changes made", :type => :boolean
|
47
|
+
|
48
|
+
################################################################
|
49
|
+
# command name mappings
|
50
|
+
|
51
|
+
map ["--version", "-v"] => :version
|
52
|
+
|
53
|
+
map ["--help", "-h"] => :help
|
54
|
+
|
55
|
+
default_command :help
|
56
|
+
|
57
|
+
################################################################
|
58
|
+
# Command: help
|
59
|
+
################################################################
|
60
|
+
|
61
|
+
desc "help [COMMAND]", "Describe available commands or one specific command"
|
62
|
+
|
63
|
+
def help(command = nil)
|
64
|
+
super(command)
|
65
|
+
end
|
66
|
+
|
67
|
+
################################################################
|
68
|
+
# Command: version
|
69
|
+
################################################################
|
70
|
+
desc "version", "Show version"
|
71
|
+
|
72
|
+
def version
|
73
|
+
puts Glima::VERSION
|
74
|
+
end
|
75
|
+
|
76
|
+
################################################################
|
77
|
+
# Command: completions
|
78
|
+
################################################################
|
79
|
+
check_unknown_options! :except => :completions
|
80
|
+
|
81
|
+
desc "completions [COMMAND]", "List available commands or options for COMMAND", :hide => true
|
82
|
+
|
83
|
+
long_desc <<-LONGDESC
|
84
|
+
List available commands or options for COMMAND
|
85
|
+
This is supposed to be a zsh compsys helper"
|
86
|
+
LONGDESC
|
87
|
+
|
88
|
+
def completions(*command)
|
89
|
+
help = self.class.commands
|
90
|
+
global_options = self.class.class_options
|
91
|
+
Glima::Command::Completions.new(help, global_options, command, config)
|
92
|
+
end
|
93
|
+
|
94
|
+
################################################################
|
95
|
+
# add some hooks to Thor
|
96
|
+
|
97
|
+
no_commands do
|
98
|
+
def invoke_command(command, *args)
|
99
|
+
setup_global_options unless command.name == "init"
|
100
|
+
result = super
|
101
|
+
teardown
|
102
|
+
result
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
################################################################
|
107
|
+
# private
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def exit_on_error(&block)
|
112
|
+
begin
|
113
|
+
yield if block_given?
|
114
|
+
rescue Glima::ConfigurationError => e
|
115
|
+
STDERR.print "ERROR: #{e.message}.\n"
|
116
|
+
exit 1
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
attr_reader :builder, :config, :client, :context, :datastore
|
121
|
+
|
122
|
+
def setup_global_options
|
123
|
+
exit_on_error do
|
124
|
+
if options[:profile]
|
125
|
+
require 'profiler'
|
126
|
+
Profiler__.start_profile
|
127
|
+
end
|
128
|
+
if options[:debug]
|
129
|
+
require "pp"
|
130
|
+
$GLIMA_DEBUG = true
|
131
|
+
$GLIMA_DEBUG_FOR_DEVELOPER = true if ENV["GLIMA_DEBUG_FOR_DEVELOPER"]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def load_plugins
|
137
|
+
config_path = options[:config] || CONFIG_PATH
|
138
|
+
plugin_dir = File.dirname(config_path)
|
139
|
+
|
140
|
+
Dir.glob(File.expand_path("plugins/*.rb", plugin_dir)) do |rb|
|
141
|
+
require rb
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def teardown
|
146
|
+
if options[:profile]
|
147
|
+
Profiler__.print_profile($stdout)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
|
4
|
+
def self.logger
|
5
|
+
@logger
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.client
|
9
|
+
@client
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_writer :logger, :client
|
14
|
+
end
|
15
|
+
|
16
|
+
dir = File.dirname(__FILE__) + "/command"
|
17
|
+
|
18
|
+
autoload :Base, "#{dir}/base.rb"
|
19
|
+
autoload :Dezip, "#{dir}/dezip.rb"
|
20
|
+
# autoload :Events, "#{dir}/events.rb"
|
21
|
+
autoload :Guess, "#{dir}/guess.rb"
|
22
|
+
autoload :Label, "#{dir}/label.rb"
|
23
|
+
autoload :Labels, "#{dir}/labels.rb"
|
24
|
+
# autoload :Open, "#{dir}/open.rb"
|
25
|
+
autoload :Profile, "#{dir}/profile.rb"
|
26
|
+
autoload :Push, "#{dir}/push.rb"
|
27
|
+
autoload :Relabel, "#{dir}/relabel.rb"
|
28
|
+
autoload :Scan, "#{dir}/scan.rb"
|
29
|
+
# autoload :Show, "#{dir}/show.rb"
|
30
|
+
autoload :Trash, "#{dir}/trash.rb"
|
31
|
+
autoload :Watch, "#{dir}/watch.rb"
|
32
|
+
autoload :Xzip, "#{dir}/xzip.rb"
|
33
|
+
|
34
|
+
end # module Command
|
35
|
+
end # module Glima
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Base
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def logger
|
8
|
+
Glima::Command.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def client
|
12
|
+
Glima::Command.client
|
13
|
+
end
|
14
|
+
|
15
|
+
def exit_if_error(message, error, logger)
|
16
|
+
return true unless error
|
17
|
+
logger.error "#{error.message.split(':').last.strip} #{message}."
|
18
|
+
exit 1
|
19
|
+
end
|
20
|
+
|
21
|
+
end # class Scan
|
22
|
+
end # module Command
|
23
|
+
end # module Glima
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Dezip < Base
|
4
|
+
|
5
|
+
def initialize(gmail_id, directory, password_file = nil, password_dir = nil)
|
6
|
+
|
7
|
+
unless File.writable?(File.expand_path(directory))
|
8
|
+
logger.error "#{directory} is not writable."
|
9
|
+
exit 1
|
10
|
+
end
|
11
|
+
|
12
|
+
mail = client.get_user_smart_message(gmail_id) do |m, err|
|
13
|
+
exit_if_error(gmail_id, err, logger)
|
14
|
+
end
|
15
|
+
|
16
|
+
# get password candidates from config file
|
17
|
+
password_candidates = []
|
18
|
+
if File.exists?(password_file)
|
19
|
+
password_candidates += File.open(password_file) {|f| f.read.split(/\n+/) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# gather password candidates from nearby mails
|
23
|
+
client.nearby_mails(mail) do |nm|
|
24
|
+
logger.info "Passwordish mail: " + nm.format_summary
|
25
|
+
password_candidates += nm.find_passwordish_strings
|
26
|
+
end
|
27
|
+
|
28
|
+
# try to unlock zip attachments
|
29
|
+
unless mail.unlock_zip!(password_candidates, logger)
|
30
|
+
logger.info "Password unlock failed."
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
|
34
|
+
# Write to unlocked zip file to DIRECTORY
|
35
|
+
mail.attachments.each do |attachment|
|
36
|
+
next unless attachment.filename =~ /\.zip$/i
|
37
|
+
zip_filename = File.expand_path(attachment.filename, directory)
|
38
|
+
Glima::Zip.new(attachment.body.decoded).write_to_file(zip_filename)
|
39
|
+
logger.info "Wrote to #{zip_filename || 'STDOUT'}."
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end # class Dezip
|
44
|
+
end # module Command
|
45
|
+
end # module Glima
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Guess < Base
|
4
|
+
|
5
|
+
def initialize(message_id)
|
6
|
+
|
7
|
+
fmt = "minimal"
|
8
|
+
user_label_ids = []
|
9
|
+
|
10
|
+
msg = client.get_user_message('me', message_id, format: fmt)
|
11
|
+
thr = client.get_user_thread('me', msg.thread_id, format: fmt)
|
12
|
+
|
13
|
+
thr.messages.each do |tmsg|
|
14
|
+
# puts tmsg.snippet
|
15
|
+
tmsg.label_ids.each do |label_id|
|
16
|
+
next unless label_id =~ /^Label_\d+$/
|
17
|
+
user_label_ids << label_id unless user_label_ids.member?(label_id)
|
18
|
+
puts "#{tmsg.id} -> #{label_id}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
user_label_ids.each do |label_id|
|
23
|
+
label = client.get_user_label(label_id)
|
24
|
+
puts "#{label_id} -> #{label.name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class Guess
|
29
|
+
end # module Command
|
30
|
+
end # module Glima
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Label < Base
|
4
|
+
|
5
|
+
def initialize(message_id, add, del)
|
6
|
+
|
7
|
+
req = {}
|
8
|
+
req[:add_label_ids] = add.map(&:id) unless add.empty?
|
9
|
+
req[:remove_label_ids] = del.map(&:id) unless add.empty?
|
10
|
+
|
11
|
+
if req.empty?
|
12
|
+
puts "Do nothing."
|
13
|
+
return 0
|
14
|
+
end
|
15
|
+
|
16
|
+
req = Google::Apis::GmailV1::ModifyMessageRequest.new(req)
|
17
|
+
|
18
|
+
client.modify_message('me', message_id, req) do |res, err|
|
19
|
+
if res
|
20
|
+
puts "Update #{message_id} successfully."
|
21
|
+
else
|
22
|
+
puts "Error: #{err}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end # class Label
|
28
|
+
end # module Command
|
29
|
+
end # module Glima
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Labels < Base
|
4
|
+
|
5
|
+
def initialize(verbose = nil)
|
6
|
+
|
7
|
+
labels = client.labels
|
8
|
+
|
9
|
+
if labels.empty?
|
10
|
+
puts 'No labels found'
|
11
|
+
return 0
|
12
|
+
end
|
13
|
+
|
14
|
+
total = labels.length
|
15
|
+
|
16
|
+
unless verbose
|
17
|
+
labels.sort_by(&:name).each do |label|
|
18
|
+
puts "#{label.name}"
|
19
|
+
end
|
20
|
+
return 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# Gmail API has rate limit at 250 requests/seccond/user (deps on type of method)
|
24
|
+
# https://developers.google.com/gmail/api/v1/reference/quota
|
25
|
+
# labels.get consumes 1quota unit
|
26
|
+
# It is only an experiment, not practical...
|
27
|
+
#
|
28
|
+
# how to retry batch requests? Issue #444 google/google-api-ruby-client
|
29
|
+
# https://github.com/google/google-api-ruby-client/issues/444
|
30
|
+
# Setting default option should also work, but it has to be done before the service is created.
|
31
|
+
#
|
32
|
+
# Retries on individual operations within a batch isn't yet
|
33
|
+
# supported. It's a bit complicated to do that correctly
|
34
|
+
# (e.g. extract the failed requests/responses, build a new batch,
|
35
|
+
# retry, repeat... merge all the results...)
|
36
|
+
#
|
37
|
+
# I'd caution against using retries with batches unless you know
|
38
|
+
# the operations are safe to repeat. Since the entire batch is
|
39
|
+
# repeated, you may be replaying successful operations as part of
|
40
|
+
# it.
|
41
|
+
#
|
42
|
+
index = 1
|
43
|
+
labels.each_slice(100) do |chunk|
|
44
|
+
client.batch do |batch_client|
|
45
|
+
chunk.each do |lbl|
|
46
|
+
batch_client.get_user_label(lbl.id) do |label, err|
|
47
|
+
if label
|
48
|
+
puts "--- #{index}/#{total} -------------------------------------------------"
|
49
|
+
puts Glima::Resource::Label.new(label).dump
|
50
|
+
index += 1
|
51
|
+
else
|
52
|
+
puts "Error: #{err}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end # chunk
|
56
|
+
end # batch
|
57
|
+
sleep 1
|
58
|
+
end # slice
|
59
|
+
end
|
60
|
+
|
61
|
+
end # class Labels
|
62
|
+
end # module Command
|
63
|
+
end # module Glima
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Profile < Base
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
response = client.get_user_profile('me')
|
7
|
+
puts "emailAddress: #{response.email_address}"
|
8
|
+
puts "messagesTotal: #{response.messages_total}"
|
9
|
+
puts "threadsTotal: #{response.threads_total}"
|
10
|
+
puts "historyId: #{response.history_id}"
|
11
|
+
end
|
12
|
+
|
13
|
+
end # class Profile
|
14
|
+
end # module Command
|
15
|
+
end # module Glima
|