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.
@@ -0,0 +1,30 @@
1
+ module Glima
2
+ class QueryParameter
3
+ class FormatError < StandardError; end
4
+
5
+ def initialize(folder, query_string, context = nil)
6
+ @params = {}
7
+ @folder, @query_string = folder, query_string
8
+
9
+ if folder == "+all"
10
+ @params[:q] = ""
11
+ elsif /^\+(\S+)/ =~ folder
12
+ @params[:q] = "in:\"#{$1}\""
13
+ else
14
+ fail "Unknown folder: #{folder}."
15
+ end
16
+
17
+ if query_string == "next"
18
+ @params[:page_token] = context&.load_page_token
19
+ raise FormatError.new("No more page") if @params[:page_token].to_s == ""
20
+ else
21
+ @params[:q] += " #{query_string}"
22
+ end
23
+ end
24
+
25
+ def to_hash
26
+ @params
27
+ end
28
+
29
+ end # class QueryParameter
30
+ end # module Glima
@@ -0,0 +1,31 @@
1
+ class String
2
+ def indent_heredoc(indent = 0)
3
+ strip_heredoc.gsub(/^/, ' ' * indent)
4
+ end
5
+
6
+ def strip_heredoc
7
+ indent = scan(/^[ \t]*(?=\S)/).min.size rescue 0
8
+ gsub(/^[ \t]{#{indent}}/, '')
9
+ end
10
+ end
11
+
12
+ module Glima
13
+ module Resource
14
+ class ParseError < StandardError; end
15
+
16
+ class Base
17
+ def initialize(raw_resource)
18
+ @raw_resource = raw_resource
19
+ end
20
+ end
21
+
22
+ dir = File.dirname(__FILE__) + "/resource"
23
+
24
+ autoload :History, "#{dir}/history.rb"
25
+ autoload :Label, "#{dir}/label.rb"
26
+ autoload :Mail, "#{dir}/mail.rb"
27
+ autoload :Message, "#{dir}/message.rb"
28
+ autoload :Thread, "#{dir}/thread.rb"
29
+ autoload :User, "#{dir}/user.rb"
30
+ end # modlue Resource
31
+ end # module Glima
@@ -0,0 +1,94 @@
1
+ module Glima
2
+ module Resource
3
+ class History < Base
4
+ class Event
5
+ attr_reader :history_id, :message, :type, :label_ids
6
+
7
+ def initialize(history_id:, message:, type:, label_ids: nil)
8
+ @history_id, @message, @type, @label_ids = history_id, message, type, label_ids
9
+ end
10
+
11
+ def dump
12
+ str = "history: #{history_id}, messgae: #{message.id}, type: #{type}"
13
+ str += ", label_ids: #{label_ids.join(',')}" if label_ids
14
+ str
15
+ end
16
+ end
17
+
18
+ # Single history entry will be converted to multiple events
19
+ def to_events
20
+ events = []
21
+ h = @raw_resource
22
+ id = h.id
23
+
24
+ h.messages_added.each do |ent|
25
+ events << Event.new(history_id: id, message: ent.message, type: :added)
26
+ end if h.messages_added
27
+
28
+ h.messages_deleted.each do |ent|
29
+ events << Event.new(history_id: id, message: ent.message, type: :deleted)
30
+ end if h.messages_deleted
31
+
32
+ h.labels_added.each do |ent|
33
+ events << Event.new(history_id: id, message: ent.message, type: :labels_added, label_ids: ent.label_ids)
34
+ end if h.labels_added
35
+
36
+ h.labels_removed.each do |ent|
37
+ events << Event.new(history_id: id, message: ent.message, type: :labels_removed, label_ids: ent.label_ids)
38
+ end if h.labels_removed
39
+
40
+ return events
41
+ end
42
+
43
+ def dump
44
+ h = @raw_resource
45
+
46
+ str = ""
47
+ types = []
48
+
49
+ msgs = h.messages
50
+ str += "** Messages: (#{msgs.length})\n"
51
+ msgs.each do |m|
52
+ str += Message.new(m).dump
53
+ end
54
+
55
+ if msgs = h.messages_added
56
+ types << :messages_added
57
+ str += "** Messages Added (#{msgs.length}):\n"
58
+ msgs.map(&:message).each do |m|
59
+ str += Message.new(m).dump
60
+ end
61
+ end
62
+
63
+ if msgs = h.messages_deleted
64
+ types << :messages_deleted
65
+ str += "** Messages Deleted (#{msgs.length}):\n"
66
+ msgs.map(&:message).each do |m|
67
+ str += Message.new(m).dump
68
+ end
69
+ end
70
+
71
+ if msgs = h.labels_added
72
+ types << :labels_added
73
+ str += "** Labels Added (#{msgs.length}):\n"
74
+ h.labels_added.each do |lm|
75
+ str += Message.new(lm.message).dump
76
+ str += " label_ids: " + lm.label_ids.join(',')
77
+ end
78
+ end
79
+
80
+ if msgs = h.labels_removed
81
+ types << :labels_removed
82
+ str += "** Labels Removed (#{msgs.length}):\n"
83
+ h.labels_removed.each do |lm|
84
+ str += Message.new(lm.message).dump
85
+ str += " label_ids: " + lm.label_ids.join(',')
86
+ end
87
+ end
88
+
89
+ return "* Id: #{h.id}, types: " + types.join(",") + "\n" + str
90
+ end
91
+
92
+ end # class History
93
+ end # module Resource
94
+ end # modlue Glima
@@ -0,0 +1,22 @@
1
+ module Glima
2
+ module Resource
3
+ class Label < Base
4
+
5
+ def dump
6
+ label = @raw_resource
7
+ str =
8
+ "id: #{label.id}\n" +
9
+ "name: #{label.name}\n" +
10
+ "messageListVisibility: #{label.message_list_visibility}\n" +
11
+ "labelListVisibility: #{label.label_list_visibility}\n" +
12
+ "type: #{label.type}\n" +
13
+ "messagesTotal: #{label.messages_total}\n" +
14
+ "messagesUnread: #{label.messages_unread}\n" +
15
+ "threadsTotal: #{label.threads_total}\n" +
16
+ "threadsUnread: #{label.threads_unread}\n"
17
+ return str
18
+ end
19
+
20
+ end # class Label
21
+ end # module Resource
22
+ end # modlue Glima
@@ -0,0 +1,155 @@
1
+ require "mail"
2
+ require 'forwardable'
3
+
4
+ module Glima
5
+ module Resource
6
+ class Mail < ::Mail::Message
7
+ extend Forwardable
8
+
9
+ def_delegators :@gmail_message,
10
+ # Users.histoy
11
+ :internal_date,
12
+ :snippet
13
+
14
+ def self.read(mail_filename)
15
+ new(File.open(mail_filename, 'rb') {|f| f.read })
16
+ end
17
+
18
+ def initialize(message)
19
+ if message.respond_to?(:raw)
20
+ @gmail_message = message
21
+ super(message.raw)
22
+ else
23
+ super(message)
24
+ end
25
+ end
26
+
27
+ def gm_msgid
28
+ @gmail_message&.id
29
+ end
30
+ alias_method :id, :gm_msgid
31
+
32
+ def gm_thrid
33
+ @gmail_message&.thread_id
34
+ end
35
+ alias_method :thread_id, :gm_thrid
36
+
37
+ def gm_label_ids
38
+ @gmail_message&.label_ids
39
+ end
40
+ alias_method :label_ids, :gm_label_ids
41
+
42
+ def raw
43
+ @gmail_message&.raw
44
+ end
45
+
46
+ def to_plain_text
47
+ mail_to_plain_text(self)
48
+ end
49
+
50
+ def find_passwordish_strings
51
+ mail = self
52
+ body = mail.to_plain_text
53
+
54
+ password_candidates = []
55
+
56
+ # gather passwordish ASCII strings.
57
+ body.scan(/(?:^|[^!-~])([!-~]{4,16})[^!-~]/) do |str|
58
+ password_candidates += str
59
+ end
60
+ return password_candidates
61
+ end
62
+
63
+ def unlock_zip!(password_candidates = [""], logger = nil)
64
+ # Unlock all zip attachments in mail
65
+ transformed = false
66
+
67
+ self.attachments.each do |attachment|
68
+ next unless attachment.filename =~ /\.zip$/i
69
+
70
+ zip = Glima::Zip.new(attachment.body.decoded)
71
+ # try all passwords
72
+ if zip.unlock_password!(password_candidates, logger)
73
+ attachment.body = zip.to_decrypted_unicode_zip.to_base64
74
+ attachment.content_transfer_encoding = "base64"
75
+ transformed = true
76
+ end
77
+ end
78
+ return transformed
79
+ end
80
+
81
+ def format_summary(count = nil)
82
+ date = Time.at(internal_date.to_i/1000).strftime("%m/%d %H:%M")
83
+ count = if count then ("%4d " % count) else "" end
84
+ return "#{count}#{date} #{id} #{CGI.unescapeHTML(snippet)[0..30]}"
85
+ end
86
+
87
+ def format_mew(count = nil)
88
+ date = Time.at(internal_date.to_i/1000).strftime("%m/%d ")
89
+
90
+ mark1 = " "
91
+ mark1 = "U" if label_ids.include?("UNREAD")
92
+
93
+ mark2 = " "
94
+ mark2 = "-" if content_type =~ /multipart\/alternative/
95
+ mark2 = "M" unless attachments.empty?
96
+
97
+ folder = File.expand_path("~/Mail/all") # XXX
98
+
99
+ summary = "#{mark1}#{mark2}#{date} #{CGI.unescapeHTML(snippet)}"
100
+ summary += "\r +#{folder} #{id} <#{id}>"
101
+ summary += if id != thread_id then " <#{thread_id}>" else " " end
102
+
103
+ return summary + " 1 2"
104
+ end
105
+
106
+ private
107
+
108
+ def mail_to_plain_text(mail)
109
+ parts = if mail.multipart? then mail.parts else [mail] end
110
+
111
+ body = parts.map do |part|
112
+ part_to_plain_text(part)
113
+ end.join("----PART----PART----PART----PART----PART----\n")
114
+
115
+ return pretty_hearder + "\n" + body
116
+ end
117
+
118
+ def part_to_plain_text(part)
119
+ case part.content_type
120
+ when /text\/plain/
121
+ convert_to_utf8(part.body.decoded.to_s,
122
+ part.content_type_parameters["charset"])
123
+ when /multipart\/alternative/
124
+ part_to_plain_text(part.text_part)
125
+
126
+ when /message\/rfc822/
127
+ mail_to_plain_text(::Mail.new(part.body.decoded.to_s))
128
+
129
+ else
130
+ "NOT_TEXT_PART (#{part.content_type})\n"
131
+ end
132
+ end
133
+
134
+ def convert_to_utf8(string, from_charset = nil)
135
+ if from_charset && from_charset != "utf-8"
136
+ string.encode("utf-8", from_charset,
137
+ :invalid => :replace, :undef => :replace)
138
+ else
139
+ string.force_encoding("utf-8")
140
+ end
141
+ end
142
+
143
+ def pretty_hearder
144
+ mail = self
145
+ ["Subject: #{mail.subject}",
146
+ "From: #{mail.header['from']&.decoded}",
147
+ "Date: #{mail.header['date']}",
148
+ "Message-Id: #{mail.header['message_id']}",
149
+ "To: #{mail.header['to']&.decoded}",
150
+ "Cc: #{mail.header['cc']&.decoded}"
151
+ ].join("\n") + "\n"
152
+ end
153
+ end # class Mail
154
+ end # module Resource
155
+ end # module Glima
@@ -0,0 +1,74 @@
1
+ module Glima
2
+ module Resource
3
+ class Message < Base
4
+
5
+ def dump
6
+ dump_message(@raw_resource)
7
+ end
8
+
9
+ private
10
+ def dump_message(msg, indent = 0)
11
+ str1 = <<-EOF.indent_heredoc(indent)
12
+ id: #{msg.id}
13
+ threadId: #{msg.thread_id}
14
+ labelIds: #{msg.label_ids&.join(', ')}
15
+ snippet: #{msg.snippet&.slice(0..20)}...
16
+ historyId: #{msg.history_id}
17
+ internalDate: #{msg.internal_date}
18
+ sizeEstimate: #{msg.size_estimate}
19
+ payload:
20
+ EOF
21
+ str1 += dump_message_part(msg.payload, indent + 2)
22
+
23
+ str2 = <<-EOF.indent_heredoc(indent)
24
+ raw:
25
+ EOF
26
+ str2 += (msg.raw.force_encoding("UTF-8")) if msg.raw
27
+ return str1 + str2
28
+ end
29
+
30
+ def dump_message_part(part, indent)
31
+ return (" " * indent) + "part is NULL\n" unless part
32
+
33
+ str1 = <<-EOF.indent_heredoc(indent)
34
+ partId: #{part.part_id}
35
+ mimeType: #{part.mime_type}
36
+ filename: #{part.filename}
37
+ headers: #{dump_message_headers(part.headers)}
38
+ body:
39
+ EOF
40
+ str1 += dump_message_attachment(part.body, indent + 2) if part.body
41
+
42
+ str2 = <<-EOF.indent_heredoc(indent)
43
+ parts:
44
+ EOF
45
+ str2 += dump_message_parts(part.parts, indent + 2)
46
+
47
+ return str1 + str2
48
+ end
49
+
50
+ def dump_message_headers(headers, all = nil)
51
+ return "headers is empty" unless headers
52
+ str = headers.map{|h| h.name + ": " + h.value}.join("\n")
53
+ return str if all
54
+ return str.split("\n").first
55
+ end
56
+
57
+ def dump_message_parts(parts, indent = 0)
58
+ return (' ' * indent) + "parts is empty\n" unless parts
59
+ return parts.map{|p| dump_message_part(p, indent)}.join("\n") + "\n"
60
+ end
61
+
62
+ def dump_message_body(body, indent)
63
+ str = <<-EOF.indent_heredoc(indent)
64
+ attachmentId: #{(body.attachment_id.to_s)[0..20]}
65
+ size: #{body&.size}
66
+ data: #{if body.data then body.data.force_encoding("UTF-8")&.gsub(/\r?\n/, "")[0..20] else 'NULL' end}...
67
+ EOF
68
+ return str
69
+ end
70
+ alias_method :dump_message_attachment, :dump_message_body
71
+
72
+ end # class Message
73
+ end # module Resource
74
+ end # module Glima
@@ -0,0 +1,3 @@
1
+ module Glima
2
+ VERSION = "0.2.1"
3
+ end
@@ -0,0 +1,156 @@
1
+ require "zip"
2
+ require "base64"
3
+
4
+ module Glima
5
+ class Zip
6
+ attr_accessor :password
7
+
8
+ def self.read(zip_filename, password = "")
9
+ new(File.open(File.expand_path(zip_file)).read, password)
10
+ end
11
+
12
+ def initialize(zip_string, password = "")
13
+ @zip_string = zip_string
14
+ @password = password
15
+ end
16
+
17
+ def correct_password?(password)
18
+ with_input_stream(password) do |zip|
19
+ begin
20
+ # Looking the first entry is not enough, because
21
+ # some zip files have directory entry which size is zero
22
+ # and no error is emitted even with wrong password.
23
+ while entry = zip.get_next_entry
24
+ size = zip.read.size # Exception if invalid password
25
+ return false if size != entry.size
26
+ return true if size > 0 # short cut
27
+ end
28
+ rescue Zlib::DataError => e
29
+ puts "*** #{e} ***" if $DEBUG
30
+ return false
31
+ end
32
+
33
+ # False-positive if all files are emtpy.
34
+ return true
35
+ end
36
+ end
37
+
38
+ def unlock_password!(password_candidates, logger = nil)
39
+ list = sort_by_password_strength(password_candidates.uniq).unshift("")
40
+
41
+ list.each do |password|
42
+ msg = "Try password:'#{password}' (#{password_strength(password)})..."
43
+
44
+ if correct_password?(password)
45
+ logger.info(msg + " OK.") if logger
46
+ @password = password
47
+ return password # Found password
48
+ else
49
+ logger.info(msg + " NG.") if logger
50
+ end
51
+ end
52
+ return nil # No luck
53
+ end
54
+
55
+ def encrypted?
56
+ correct_password?("")
57
+ end
58
+
59
+ def write_to_file(file)
60
+ return file.write(@zip_string) if file.respond_to?(:write)
61
+
62
+ File.open(file, "w") do |f|
63
+ f.write(@zip_string)
64
+ end
65
+ end
66
+
67
+ def to_s
68
+ @zip_string
69
+ end
70
+
71
+ def to_base64
72
+ Base64.encode64(@zip_string)
73
+ end
74
+
75
+ def to_decrypted_unicode_zip()
76
+ ::Zip.unicode_names = true
77
+
78
+ out = ::Zip::OutputStream.write_buffer(StringIO.new) do |zos|
79
+ with_input_stream(@password) do |zis|
80
+ while entry = zis.get_next_entry
81
+ name = cp932_path_to_utf8_path(entry.name)
82
+
83
+ # Two types of Exception will occur on encrypted zip:
84
+ # 1) "invalid block type (Zlib::DataError)" if password is not specified.
85
+ # 2) "invalid stored block lengths (Zlib::DataError)" if password is wrong.
86
+ content = zis.read
87
+ raise Zlib::DataError if content.size != entry.size
88
+
89
+ zos.put_next_entry(name)
90
+ zos.write(content)
91
+ end
92
+ end
93
+ end
94
+ Zip.new(out.string)
95
+ end
96
+
97
+ private
98
+
99
+ def password_strength(password)
100
+ password = password.to_s
101
+ score = Math.log2(password.length + 1)
102
+
103
+ password.scan(/[a-z]+|[A-Z]+|\d+|[!"#$%&'()*+,-.\/:;<=>?@\[\\\]^_`{|}~]+/) do |s|
104
+ score += 1.0
105
+ end
106
+ return score
107
+ end
108
+
109
+ def sort_by_password_strength(password_array)
110
+ password_array.sort{|a,b|
111
+ password_strength(b) <=> password_strength(a)
112
+ }
113
+ end
114
+
115
+ def with_input_stream(password = "", &block)
116
+ ::Zip::InputStream.open(StringIO.new(@zip_string), 0, decrypter(password)) do |zis|
117
+ yield zis
118
+ end
119
+ end
120
+
121
+ def decrypter(password = "")
122
+ if password.empty?
123
+ nil # return empty decrypter
124
+ else
125
+ ::Zip::TraditionalDecrypter.new(password)
126
+ end
127
+ end
128
+
129
+ # 1) Convert CP932 (SJIS) to UTF8.
130
+ # 2) Replace path-separators from backslash (\) to slash (/).
131
+ #
132
+ # Example:
133
+ # path = io.get_next_entry.name # rubyzip returns ASCII-8BIT string as name.
134
+ # path is:
135
+ # + ASCII-8BIT
136
+ # + Every backslash is replaced to '/' even in second-byte of CP932.
137
+ #
138
+ # See also:
139
+ # https://github.com/rubyzip/rubyzip/blob/master/lib/zip/entry.rb#L223
140
+ # Zip::Entry#read_local_entry does gsub('\\', '/')
141
+ #
142
+ def cp932_path_to_utf8_path(cp932_path_string)
143
+ # Replace-back all '/' to '\'
144
+ name = cp932_path_string.force_encoding("BINARY").gsub('/', '\\')
145
+
146
+ # Change endoding to CP932 (SJIS) and replace all '\' to '/'
147
+ # In this replacement, '\' in second-byte of CP932 will be preserved.
148
+ name = name.force_encoding("CP932").gsub('\\', '/')
149
+
150
+ # Convert CP932 to UTF-8
151
+ return name.encode("utf-8", "CP932",
152
+ :invalid => :replace, :undef => :replace)
153
+ end
154
+
155
+ end # class Zip
156
+ end # module Glima