glima 0.2.1

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