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
@@ -0,0 +1,26 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Push < Base
|
4
|
+
|
5
|
+
def initialize(email_file, date, thread, labels)
|
6
|
+
label_ids = labels.map(&:id) + ["INBOX", "UNREAD"]
|
7
|
+
|
8
|
+
File.open(email_file) do |source|
|
9
|
+
client.insert_user_message(
|
10
|
+
'me',
|
11
|
+
Google::Apis::GmailV1::Message.new(label_ids: label_ids, thread_id: thread),
|
12
|
+
content_type: "message/rfc822",
|
13
|
+
internal_date_source: date,
|
14
|
+
upload_source: source) do |msg, err|
|
15
|
+
if msg
|
16
|
+
puts "pushed to: #{msg.id}"
|
17
|
+
else
|
18
|
+
STDERR.puts "Error: #{err}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end # class Push
|
25
|
+
end # module Command
|
26
|
+
end # module Glima
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Relabel < Base
|
4
|
+
|
5
|
+
def initialize(source_name, dest_name, dry_run)
|
6
|
+
|
7
|
+
all_labels = client.list_user_labels('me').labels.sort_by(&:name)
|
8
|
+
|
9
|
+
if /\/$/ =~ dest_name
|
10
|
+
move_to_dir = true
|
11
|
+
dest_name = dest_name.sub(/\/$/, '')
|
12
|
+
else
|
13
|
+
move_to_dir = false
|
14
|
+
end
|
15
|
+
|
16
|
+
source_labels = all_labels.find_all {|x| File.fnmatch(source_name, x.name, File::FNM_PATHNAME)}
|
17
|
+
dest_label = all_labels.find {|x| x.name == dest_name}
|
18
|
+
|
19
|
+
if source_labels.empty?
|
20
|
+
puts "Error: source #{source_name} not found"
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
|
24
|
+
if dest_label && !move_to_dir
|
25
|
+
puts "Error: dest #{dest_name} already exists"
|
26
|
+
return nil
|
27
|
+
end
|
28
|
+
|
29
|
+
if !dest_label && move_to_dir
|
30
|
+
puts "Error: dest #{dest_name} not found"
|
31
|
+
return nil
|
32
|
+
end
|
33
|
+
|
34
|
+
source_labels.each do |source_label|
|
35
|
+
dirtop = File.dirname(source_label.name)
|
36
|
+
sub_labels = all_labels.find_all {|x| File.fnmatch(source_label.name + '/*', x.name)}
|
37
|
+
|
38
|
+
([source_label] + sub_labels).each do |label|
|
39
|
+
src = label.name
|
40
|
+
dst = dest_name + '/' + (label.name.sub(/^#{dirtop}\//, ''))
|
41
|
+
|
42
|
+
if all_labels.find {|x| x.name == dst}
|
43
|
+
puts "Error: relabel #{src} -> #{dst}: Destination already exists"
|
44
|
+
next
|
45
|
+
else
|
46
|
+
puts "relabel #{src} -> #{dst}"
|
47
|
+
end
|
48
|
+
|
49
|
+
unless dry_run
|
50
|
+
label_obj = Google::Apis::GmailV1::Label.new(id: label.id, name: dst)
|
51
|
+
client.patch_user_label('me', label.id, label_obj) do |response, err|
|
52
|
+
if response
|
53
|
+
# puts dump_label(response)
|
54
|
+
else
|
55
|
+
puts "Error: #{err}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end # class Relabel
|
64
|
+
end # module Command
|
65
|
+
end # module Glima
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Scan < Base
|
4
|
+
|
5
|
+
def initialize(folder, format, search_or_range)
|
6
|
+
|
7
|
+
index = 1
|
8
|
+
client.scan_batch(folder, search_or_range) do |mail|
|
9
|
+
case format
|
10
|
+
when :mew
|
11
|
+
puts mail.format_mew(index)
|
12
|
+
when :archive
|
13
|
+
puts format_archive_friendly(mail)
|
14
|
+
else
|
15
|
+
puts mail.format_summary(index)
|
16
|
+
end
|
17
|
+
index += 1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def format_archive_friendly(mail)
|
24
|
+
date = mail.date.strftime("%Y-%m-%d-%H%M%S")
|
25
|
+
# Replace unsafe chars for filename
|
26
|
+
subject = mail.subject.tr('!/:*?"<>|\\', '!/:*?″<>|\').gsub(/[\s ]/, '')
|
27
|
+
return "#{mail.id} #{date}-#{mail.id}-#{subject}.eml"
|
28
|
+
end
|
29
|
+
|
30
|
+
end # class Scan
|
31
|
+
end # module Command
|
32
|
+
end # module Glima
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Trash < Base
|
4
|
+
|
5
|
+
def initialize(message_ids)
|
6
|
+
|
7
|
+
client.batch do |batch_client|
|
8
|
+
message_ids.each do |id|
|
9
|
+
batch_client.trash_user_message(id) do |res, err|
|
10
|
+
if res
|
11
|
+
puts "Trash #{id} successfully."
|
12
|
+
else
|
13
|
+
puts "Error: #{err}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end # class Trash
|
21
|
+
end # module Command
|
22
|
+
end # module Glima
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Watch < Base
|
4
|
+
|
5
|
+
def initialize(queue_label = nil, mark_label = nil)
|
6
|
+
|
7
|
+
# 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,
|
37
|
+
add_dst_labels: add_labels,
|
38
|
+
del_dst_labels: del_labels,
|
39
|
+
del_src_labels: del_labels)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end # class Watch
|
44
|
+
end # module Command
|
45
|
+
end # module Glima
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Glima
|
2
|
+
module Command
|
3
|
+
class Xzip < Base
|
4
|
+
def initialize(target,
|
5
|
+
add_src_labels: [],
|
6
|
+
del_src_labels: [],
|
7
|
+
add_dst_labels: [],
|
8
|
+
del_dst_labels: [])
|
9
|
+
|
10
|
+
add_src_label_ids = add_src_labels.map(&:id)
|
11
|
+
del_src_label_ids = del_src_labels.map(&:id)
|
12
|
+
add_dst_label_ids = add_dst_labels.map(&:id)
|
13
|
+
del_dst_label_ids = del_dst_labels.map(&:id)
|
14
|
+
|
15
|
+
ids = if target =~ /^[\da-fA-F]{16}$/
|
16
|
+
[target]
|
17
|
+
else
|
18
|
+
client.find_messages(target)
|
19
|
+
end
|
20
|
+
|
21
|
+
ids.each do |message_id|
|
22
|
+
# get target mail
|
23
|
+
mail = client.get_user_smart_message(message_id) do |m, err|
|
24
|
+
if err
|
25
|
+
puts "Error: #{err}"
|
26
|
+
next
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# find password candidates from nearby mails
|
31
|
+
password_candidates = []
|
32
|
+
client.nearby_mails(mail) do |nm|
|
33
|
+
logger.info "Passwordish mail: " + nm.format_summary
|
34
|
+
password_candidates += nm.find_passwordish_strings
|
35
|
+
end
|
36
|
+
|
37
|
+
# try to unlock zip attachments
|
38
|
+
unless mail.unlock_zip!(password_candidates, logger)
|
39
|
+
puts "Password unlock failed."
|
40
|
+
next
|
41
|
+
end
|
42
|
+
|
43
|
+
# push back unlocked mail to server
|
44
|
+
unless push_mail(mail, "dateHeader", add_dst_label_ids, del_dst_label_ids)
|
45
|
+
puts "Push mail failed."
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
# add/remove labels from the target
|
50
|
+
if add_src_label_ids.empty? && del_src_label_ids.empty?
|
51
|
+
next
|
52
|
+
end
|
53
|
+
|
54
|
+
req = {}
|
55
|
+
req[:add_label_ids] = add_src_label_ids
|
56
|
+
req[:remove_label_ids] = del_src_label_ids
|
57
|
+
|
58
|
+
req = Google::Apis::GmailV1::ModifyMessageRequest.new(req)
|
59
|
+
|
60
|
+
client.modify_message('me', message_id, req) do |res,err|
|
61
|
+
if res
|
62
|
+
puts "Update #{message_id} successfully."
|
63
|
+
else
|
64
|
+
puts "Error: #{err}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end # def initialize
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def push_mail(mail, date_source = "receivedTime", add_label_ids = [], del_label_ids = [])
|
73
|
+
label_ids = (mail.label_ids +
|
74
|
+
add_label_ids -
|
75
|
+
del_label_ids +
|
76
|
+
["INBOX", "UNREAD"]).uniq
|
77
|
+
thid = mail.thread_id
|
78
|
+
|
79
|
+
unless date_source == "dateHeader" || date_source == "receivedTime"
|
80
|
+
raise "Unknown date type: #{date_source}"
|
81
|
+
end
|
82
|
+
|
83
|
+
mail.header["X-Glima-Processed"] = DateTime.now.rfc2822
|
84
|
+
|
85
|
+
client.insert_user_message(
|
86
|
+
'me',
|
87
|
+
Google::Apis::GmailV1::Message.new(label_ids: label_ids, thread_id: thid),
|
88
|
+
content_type: "message/rfc822",
|
89
|
+
internal_date_source: date_source,
|
90
|
+
upload_source: StringIO.new(mail.to_s)) do |msg, err|
|
91
|
+
if msg
|
92
|
+
puts "pushed to: #{msg.id}"
|
93
|
+
return true
|
94
|
+
else
|
95
|
+
STDERR.puts "Error: #{err}"
|
96
|
+
return false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end # class Xzip
|
102
|
+
end # module Command
|
103
|
+
end # module Glima
|
data/lib/glima/config.rb
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
module Glima
|
5
|
+
class Config
|
6
|
+
# Syntax table manipulation
|
7
|
+
class Syntax
|
8
|
+
def initialize(syntax_config)
|
9
|
+
@syntax_config = syntax_config
|
10
|
+
end
|
11
|
+
|
12
|
+
def keyword_symbols
|
13
|
+
@syntax_config.keys
|
14
|
+
end
|
15
|
+
|
16
|
+
def keywords
|
17
|
+
keyword_symbols.map {|sym| sym.to_s.upcase }
|
18
|
+
end
|
19
|
+
|
20
|
+
def keyword?(word)
|
21
|
+
if word.is_a?(Symbol)
|
22
|
+
keyword_symbols.member?(word)
|
23
|
+
else
|
24
|
+
# String
|
25
|
+
keywords.member?(word)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def instance_variable_name(word)
|
30
|
+
return nil unless keyword?(word)
|
31
|
+
return '@' + as_symbol(word).to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def item_class(word)
|
35
|
+
return nil unless keyword?(word)
|
36
|
+
@syntax_config[as_symbol(word)]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def as_symbol(word)
|
41
|
+
word.to_s.downcase.sub(/^@+/, "").to_sym
|
42
|
+
end
|
43
|
+
end # class Syntax
|
44
|
+
|
45
|
+
# Parse Key-Value object in YAML
|
46
|
+
class Base
|
47
|
+
# attr_accessor :name
|
48
|
+
|
49
|
+
def self.create_from_yaml_file(yaml_file)
|
50
|
+
yaml_string = File.open(File.expand_path(yaml_file)).read
|
51
|
+
return create_from_yaml_string(yaml_string, yaml_file)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.create_from_yaml_string(yaml_string, filename = nil)
|
55
|
+
hash = YAML.load(yaml_string, filename) || {}
|
56
|
+
return new(hash)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.define_syntax(config)
|
60
|
+
@syntax = Syntax.new(config)
|
61
|
+
@syntax.keyword_symbols.each do |sym|
|
62
|
+
attr_accessor sym # XXX: attr_reader is enough?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.syntax
|
67
|
+
return @syntax
|
68
|
+
end
|
69
|
+
|
70
|
+
def initialize(hash = {})
|
71
|
+
@original_hash = hash
|
72
|
+
(hash || {}).each do |key, val|
|
73
|
+
raise Glima::ConfigurationError, "config syntax error (#{key})" unless syntax.keyword?(key)
|
74
|
+
var = syntax.instance_variable_name(key)
|
75
|
+
obj = create_subnode(key, val)
|
76
|
+
instance_variable_set(var, obj)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_reader :original_hash
|
81
|
+
|
82
|
+
def get_value(dot_separated_string = nil)
|
83
|
+
if dot_separated_string.to_s == ""
|
84
|
+
return original_hash
|
85
|
+
end
|
86
|
+
|
87
|
+
key, subkey = dot_separated_string.to_s.upcase.split(".", 2)
|
88
|
+
subnode = get_subnode(key)
|
89
|
+
|
90
|
+
if subnode.respond_to?(:get_value)
|
91
|
+
return subnode.get_value(subkey)
|
92
|
+
else
|
93
|
+
return subnode.to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_yaml
|
98
|
+
return self.to_hash.to_yaml
|
99
|
+
end
|
100
|
+
|
101
|
+
def to_hash
|
102
|
+
hash = {}
|
103
|
+
syntax.keywords.each do |key|
|
104
|
+
var = syntax.instance_variable_name(key)
|
105
|
+
obj = instance_variable_get(var)
|
106
|
+
obj = obj.respond_to?(:to_hash) ? obj.to_hash : obj.to_s
|
107
|
+
hash[key] = obj
|
108
|
+
end
|
109
|
+
return hash
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
def syntax
|
114
|
+
self.class.syntax
|
115
|
+
end
|
116
|
+
|
117
|
+
def get_subnode(key)
|
118
|
+
raise Glima::ConfigurationError, "Invalid key: #{key}" unless syntax.keyword?(key)
|
119
|
+
return instance_variable_get(syntax.instance_variable_name(key))
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_subnode(keyword, value)
|
123
|
+
item_class = syntax.item_class(keyword)
|
124
|
+
if item_class.is_a?(Array)
|
125
|
+
return List.new(item_class.first, value)
|
126
|
+
elsif item_class == String
|
127
|
+
return value.to_s
|
128
|
+
else
|
129
|
+
return item_class.new(value)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end # class Base
|
134
|
+
|
135
|
+
# Parse Array object in YAML
|
136
|
+
class List < Base
|
137
|
+
include Enumerable
|
138
|
+
|
139
|
+
def initialize(item_class, array = [])
|
140
|
+
@original_hash = array
|
141
|
+
@configs = []
|
142
|
+
(array || []).each do |value|
|
143
|
+
item = item_class.new(value)
|
144
|
+
@configs << item
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def [](key)
|
149
|
+
@configs.find {|c| c.name == key}
|
150
|
+
end
|
151
|
+
|
152
|
+
alias_method :get_subnode, :[]
|
153
|
+
|
154
|
+
def <<(conf)
|
155
|
+
@configs << conf
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_hash # XXX: actually, it returns a Array
|
159
|
+
return @configs.map {|c| c.respond_to?(:to_hash) ? c.to_hash : c.to_s}
|
160
|
+
end
|
161
|
+
|
162
|
+
def each
|
163
|
+
@configs.each do |conf|
|
164
|
+
yield conf
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end # List
|
168
|
+
|
169
|
+
## concrete config classes
|
170
|
+
|
171
|
+
class General < Base
|
172
|
+
define_syntax :client_id => String,
|
173
|
+
:client_secret => String,
|
174
|
+
:token_store => String,
|
175
|
+
:context_store => String,
|
176
|
+
:zip_passwords_file => String,
|
177
|
+
:default_user => String
|
178
|
+
end # class General
|
179
|
+
|
180
|
+
# Top-Level Config
|
181
|
+
class Top < Base
|
182
|
+
define_syntax :general => General
|
183
|
+
end # class Top
|
184
|
+
|
185
|
+
def self.create_from_file(file_name)
|
186
|
+
unless File.exists?(File.expand_path(file_name))
|
187
|
+
raise Glima::ConfigurationError, "config file '#{file_name}' not found"
|
188
|
+
end
|
189
|
+
begin
|
190
|
+
return Top.create_from_yaml_file(file_name)
|
191
|
+
rescue Psych::SyntaxError => e
|
192
|
+
raise Glima::ConfigurationError, e.message
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def self.create_from_string(string)
|
197
|
+
begin
|
198
|
+
return Top.create_from_yaml_string(string)
|
199
|
+
rescue Psych::SyntaxError => e
|
200
|
+
raise Glima::ConfigurationError, e.message
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end # class Config
|
205
|
+
end # module Glima
|