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
@@ -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
|