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