hermeneutics 1.11 → 1.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,240 @@
1
+ #
2
+ # hermeneutics/cli/imap.rb -- IMAP client
3
+ #
4
+
5
+ require "hermeneutics/cli/protocol"
6
+ require "hermeneutics/cli/imap/commands"
7
+
8
+
9
+ module Hermeneutics
10
+
11
+ module Cli
12
+
13
+ class IMAP < Protocol
14
+
15
+ CRLF = true
16
+
17
+ PORT, PORT_SSL = 143, 993
18
+
19
+ class Error < StandardError ; end
20
+ class UnspecResponse < Error ; end
21
+ class ServerBye < Error ; end
22
+ class ServerError < Error ; end
23
+ class NotOk < Error ; end
24
+
25
+ class <<self
26
+ private :new
27
+ def open host, port = nil, timeout: nil, ssl: false
28
+ port ||= ssl ? PORT_SSL : PORT
29
+ super host, port, timeout: timeout, ssl: ssl do |i|
30
+ yield i
31
+ i.stop_watch
32
+ end
33
+ end
34
+ end
35
+
36
+ TAG_PREFIX = "H"
37
+
38
+ def initialize *args
39
+ super
40
+ @tag = "H%04d" % 0
41
+ @info = [ get_response]
42
+ start_watch
43
+ end
44
+
45
+ attr_reader :info
46
+
47
+ def auths data = nil
48
+ a = []
49
+ (data||@info.first.data).params.each { |p|
50
+ p =~ /\AAUTH=/ and a.push $'.to_s
51
+ }
52
+ a
53
+ end
54
+
55
+ def command cmd, *args, &block
56
+ c = cmd.new *args
57
+ r = write_request c, &block
58
+ r.ok? or raise NotOk, r.text
59
+ c.responses
60
+ end
61
+
62
+
63
+ include ImapTools
64
+
65
+ alias readline! readline
66
+ private :readline!
67
+
68
+ def peekline
69
+ @peek ||= readline!
70
+ end
71
+
72
+ def readline
73
+ @peek or readline!
74
+ ensure
75
+ @peek = nil
76
+ end
77
+
78
+
79
+ def get_response_plain
80
+ Response.create @tag, self
81
+ end
82
+
83
+ def get_response
84
+ r = get_response_plain
85
+ r.bye? and raise ServerBye, "Server closed the connection"
86
+ r
87
+ end
88
+
89
+
90
+ def start_watch
91
+ @watch = Thread.new do
92
+ Thread.current.abort_on_exception = true
93
+ Thread.current.report_on_exception = false
94
+ while @socket.wait do
95
+ r = get_response
96
+ @info.push r
97
+ end
98
+ end
99
+ end
100
+
101
+ def stop_watch
102
+ @watch or return
103
+ @watch.kill if @watch.alive?
104
+ @watch.value
105
+ @watch = nil
106
+ end
107
+
108
+ def write_request cmd
109
+ stop_watch
110
+ @tag.succ!
111
+ r = nil
112
+ cmd.stream_lines "#@tag" do |a|
113
+ a.each { |l| writeline l.to_s }
114
+ r = get_response_plain
115
+ r.wait? or break
116
+ if block_given? then # does only make sense for the IDLE command
117
+ begin
118
+ start_watch
119
+ yield
120
+ ensure
121
+ stop_watch
122
+ end
123
+ end
124
+ r.text
125
+ end
126
+ until r.done? do
127
+ bye ||= r.bye?
128
+ cmd.add_response r
129
+ r = get_response_plain
130
+ r or raise ServerError, cmd.responses.last.to_s
131
+ end
132
+ bye or start_watch
133
+ r
134
+ end
135
+
136
+ end
137
+
138
+ module ImapTools
139
+
140
+ class Response
141
+ class <<self
142
+ private :new
143
+ def create tag, reader
144
+ p = reader.peekline
145
+ p and p.slice! /\A(\S+) +/ or return
146
+ case $1
147
+ when tag then ResponseFinish.create reader
148
+ when "+" then ResponseWait. create reader
149
+ when "*" then ResponseStatus.create reader or
150
+ ResponseData. create reader
151
+ else raise UnspecResponse, reader.readline
152
+ end
153
+ end
154
+ private
155
+ def compile_string str
156
+ r = StringReader.new str
157
+ compile_stream r
158
+ end
159
+ def compile_stream reader
160
+ c = Data::Compiler.new
161
+ Parser.compile reader, c
162
+ end
163
+ end
164
+ def done? ; false ; end
165
+ def wait? ; false ; end
166
+ def bye? ; false ; end
167
+ end
168
+
169
+ class ResponseWait < Response
170
+ class <<self
171
+ def create reader
172
+ new reader.readline
173
+ end
174
+ end
175
+ attr_reader :text
176
+ def initialize text ; @text = text ; end
177
+ def wait? ; true ; end
178
+ def to_s ; "#{text}" ; end
179
+ end
180
+
181
+ class ResponseData < Response
182
+ class <<self
183
+ def create reader
184
+ if reader.peekline.slice! /\A(\d+) +/ then
185
+ n = $1.to_i
186
+ end
187
+ data = compile_stream reader
188
+ new n, data
189
+ end
190
+ end
191
+ attr_reader :num, :data
192
+ def initialize num, data
193
+ @num, @data = num, data
194
+ end
195
+ def to_s ; "#{num} #{data}" ; end
196
+ end
197
+
198
+ class ResponseStatus < Response
199
+ class <<self
200
+ def create reader
201
+ l = compile_line reader
202
+ new *l if l
203
+ end
204
+ private
205
+ def compile_line reader
206
+ if reader.peekline.slice! /\A(OK|NO|BAD|BYE|PREAUTH) +/ then
207
+ status = $1.to_sym
208
+ if reader.peekline.slice! /\[(.*)\] +/ then
209
+ data = compile_string $1
210
+ end
211
+ [ status, data, reader.readline]
212
+ end
213
+ end
214
+ end
215
+ attr_reader :status, :data, :text
216
+ def initialize status, data, text
217
+ @status, @data, @text = status, data, text
218
+ end
219
+ def ok? ; @status == :OK ; end
220
+ def bye? ; @status == :BYE ; end
221
+ def to_s ; "#{status} #{data} #{text}" ; end
222
+ end
223
+
224
+ class ResponseFinish < ResponseStatus
225
+ class <<self
226
+ def create reader
227
+ l = compile_line reader
228
+ new *l if l
229
+ end
230
+ end
231
+ def done? ; true ; end
232
+ def to_s ; "finished" ; end
233
+ end
234
+
235
+ end
236
+
237
+ end
238
+
239
+ end
240
+
@@ -0,0 +1,11 @@
1
+ require "openssl"
2
+
3
+ module OpenSSL
4
+ module SSL
5
+ class SSLSocket
6
+ def wait timeout = :read ; @io.wait timeout ; end
7
+ def ready? ; @io.ready? ; end
8
+ end
9
+ end
10
+ end
11
+
@@ -0,0 +1,257 @@
1
+ #
2
+ # hermeneutics/cli/pop3.rb -- POP3 client
3
+ #
4
+
5
+ require "hermeneutics/cli/protocol"
6
+
7
+ module Hermeneutics
8
+
9
+ module Cli
10
+
11
+ class POP3 < Protocol
12
+
13
+ CRLF = true
14
+
15
+ PORT, PORT_SSL = 110, 995
16
+
17
+ class Error < StandardError ; end
18
+ class UnspecError < Error ; end
19
+ class AuthFail < Error ; end
20
+ class Check < Error ; end
21
+ class Unused < Error ; end
22
+
23
+ class <<self
24
+ private :new
25
+ def open host, port = nil, timeout: nil, ssl: false
26
+ port ||= ssl ? PORT_SSL : PORT
27
+ super host, port, timeout: timeout, ssl: ssl
28
+ end
29
+ end
30
+
31
+ attr_reader :last_response
32
+
33
+ def initialize *args
34
+ super
35
+ @stamp = get_response.slice /<[!-~]+@[!-~]+>/
36
+ end
37
+
38
+ def authenticate name, pwd
39
+ if @stamp then
40
+ apop name, pwd
41
+ else
42
+ user name
43
+ pass pwd
44
+ end
45
+ end
46
+
47
+ def user name
48
+ writeline "USER #{name}"
49
+ get_response
50
+ end
51
+
52
+ def pass pwd
53
+ writeline "PASS #{pwd}"
54
+ get_response_auth
55
+ end
56
+
57
+ def apop name, pwd
58
+ require "digest/md5"
59
+ hash = Digest::MD5.hexdigest "#@stamp#{pwd}"
60
+ writeline "APOP #{name} #{hash}"
61
+ get_response_auth
62
+ end
63
+
64
+ def capa
65
+ if block_given? then
66
+ writeline "CAPA"
67
+ get_response do |_|
68
+ get_data { |l|
69
+ c, *rest = l.split
70
+ yield c, rest
71
+ }
72
+ end
73
+ else
74
+ r = Hash.new do |h,k| h[k] = [] end
75
+ capa do |c,v|
76
+ if v.notempty? then
77
+ r[c].concat v
78
+ else
79
+ r[c] = true
80
+ end
81
+ end
82
+ r
83
+ end
84
+ end
85
+
86
+ def stat
87
+ if block_given? then
88
+ writeline "STAT"
89
+ n, r = split_num_len get_response
90
+ yield n, r
91
+ else
92
+ stat do |*a| a end
93
+ end
94
+ end
95
+
96
+ def list n = nil
97
+ n = n.to_i.nonzero?
98
+ if block_given? then
99
+ cmd = "LIST"
100
+ cmd << " #{n}" if n
101
+ writeline cmd
102
+ if n then
103
+ n_, len = split_num_len get_response
104
+ n == n_ or raise Check, "Wrong LIST response: #{n} <-> #{n_}"
105
+ yield n, len
106
+ else
107
+ get_response do |_|
108
+ get_data do |l|
109
+ n_, len = split_num_len l
110
+ yield n_, len
111
+ end
112
+ end
113
+ end
114
+ else
115
+ if n then
116
+ list n do |*a| a end
117
+ else
118
+ h = {}
119
+ list n do |n_,len|
120
+ h[ n_] = len
121
+ end
122
+ h
123
+ end
124
+ end
125
+ end
126
+
127
+ def uidl n = nil
128
+ n = n.to_i.nonzero?
129
+ if block_given? then
130
+ cmd = "UIDL"
131
+ cmd << " #{n}" if n
132
+ writeline cmd
133
+ if n then
134
+ n_, id = split_num get_response
135
+ n == n_ or raise Check, "Wrong UIDL response: #{n} <-> #{n_}"
136
+ yield n, id
137
+ else
138
+ get_response do |_|
139
+ get_data do |l|
140
+ n_, id = split_num l
141
+ yield n_, id
142
+ end
143
+ end
144
+ end
145
+ else
146
+ if n then
147
+ uidl n do |*a| a end
148
+ else
149
+ h = {}
150
+ uidl n do |n_,id|
151
+ h[ n_] = id
152
+ end
153
+ h
154
+ end
155
+ end
156
+ end
157
+
158
+ def retr n, &block
159
+ writeline "RETR #{n}"
160
+ get_response do |_|
161
+ get_data_str &block
162
+ end
163
+ end
164
+
165
+ def top n, x, &block
166
+ writeline "TOP #{n} #{x}"
167
+ get_response do |_|
168
+ get_data_str &block
169
+ end
170
+ end
171
+
172
+
173
+ def dele n
174
+ writeline "DELE #{n}"
175
+ get_response
176
+ end
177
+
178
+ def rset
179
+ writeline "RSET"
180
+ get_response
181
+ end
182
+
183
+ def noop
184
+ writeline "NOOP"
185
+ get_response
186
+ end
187
+
188
+ def quit
189
+ writeline "QUIT"
190
+ get_response
191
+ end
192
+
193
+ private
194
+
195
+ def get_response
196
+ r = readline
197
+ a = case r
198
+ when /^\+OK */ then @last_response = $'.notempty?
199
+ when /^\-ERR */ then raise Error, $'
200
+ else raise UnspecError, r
201
+ end
202
+ if block_given? then
203
+ yield a
204
+ else
205
+ a
206
+ end
207
+ ensure
208
+ unless done? then
209
+ r = readline
210
+ r and raise Unused, "Unexpected data: #{r.inspect}"
211
+ end
212
+ end
213
+
214
+ def get_response_auth
215
+ begin
216
+ get_response
217
+ rescue Error
218
+ err = $!.message
219
+ end
220
+ raise AuthFail, err if err
221
+ end
222
+
223
+ def get_data
224
+ loop do
225
+ l = readline
226
+ break if l == "."
227
+ l.slice /\A\./
228
+ yield l
229
+ end
230
+ end
231
+
232
+ def get_data_str
233
+ if block_given? then
234
+ get_data { |l| yield l }
235
+ else
236
+ r = ""
237
+ get_data { |l| r << l << "\n" }
238
+ r
239
+ end
240
+ end
241
+
242
+ def split_num str
243
+ n, r = str.split nil, 2
244
+ [ n.to_i, r]
245
+ end
246
+
247
+ def split_num_len str
248
+ n, r = split_num str
249
+ [ n, r.to_i]
250
+ end
251
+
252
+ end
253
+
254
+ end
255
+
256
+ end
257
+
@@ -0,0 +1,141 @@
1
+ #
2
+ # hermeneutics/cli/protocol.rb -- Basic communication
3
+ #
4
+
5
+ require "supplement"
6
+ require "socket"
7
+
8
+
9
+ if RUBY_VERSION < "3" then
10
+ class TCPSocket
11
+ class <<self
12
+ alias open_orig open
13
+ def open host, port, connect_timeout: nil, &block
14
+ open_orig host, port, &block
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ module Hermeneutics
21
+
22
+ module Cli
23
+
24
+ class Protocol
25
+
26
+ class <<self
27
+ private :new
28
+ def open host, port, timeout: nil, ssl: false
29
+ open_socket host, port, timeout, ssl do |s|
30
+ i = new s, timeout
31
+ yield i
32
+ end
33
+ end
34
+ private
35
+ def open_socket host, port, timeout, ssl
36
+ TCPSocket.open host, port, connect_timeout: timeout do |s|
37
+ if ssl then
38
+ require "hermeneutics/cli/openssl"
39
+ if Hash === ssl then
40
+ if ssl[ :ca_file] || ssl[ :ca_path] then
41
+ ssl[ :verify_mode] ||= OpenSSL::SSL::VERIFY_PEER
42
+ end
43
+ else
44
+ vfm = case ssl
45
+ when true then OpenSSL::SSL::VERIFY_NONE
46
+ when Integer then ssl
47
+ when :none then OpenSSL::SSL::VERIFY_NONE
48
+ when :peer then OpenSSL::SSL::VERIFY_PEER
49
+ end
50
+ ssl = { verify_mode: vfm}
51
+ end
52
+ ctx = OpenSSL::SSL::SSLContext.new
53
+ ctx.set_params ssl
54
+ s = OpenSSL::SSL::SSLSocket.new s, ctx
55
+ s.connect
56
+ end
57
+ yield s
58
+ end
59
+ end
60
+ end
61
+
62
+ CRLF = false
63
+
64
+ attr_writer :timeout
65
+
66
+ def initialize socket, timeout
67
+ @socket, @timeout = socket, timeout
68
+ end
69
+
70
+ def trace!
71
+ @trace = true
72
+ end
73
+
74
+ def writeline l
75
+ l.chomp!
76
+ @trace and $stderr.puts "C: #{l}"
77
+ @socket.write l
78
+ @socket.write self.class::CRLF ? "\r\n" : "\n"
79
+ end
80
+
81
+ def readline
82
+ @socket.wait @timeout||0
83
+ r = @socket.readline
84
+ r.chomp!
85
+ @trace and $stderr.puts "S: #{r}"
86
+ r
87
+ rescue EOFError
88
+ end
89
+
90
+ def write data
91
+ @trace and $stderr.puts "C- #{data.inspect}"
92
+ @socket.write data
93
+ end
94
+
95
+ def read bytes
96
+ @socket.wait @timeout||0
97
+ r = @socket.read bytes
98
+ @trace and $stderr.puts "S- #{r.inspect}"
99
+ r
100
+ rescue EOFError
101
+ end
102
+
103
+ def done?
104
+ not @socket.ready?
105
+ end
106
+
107
+ end
108
+
109
+
110
+ module CramMD5
111
+
112
+ class <<self
113
+ def included cls
114
+ require "digest/md5"
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def crammd5_answer a
121
+ "#@user #{hmac_md5 a, @passwd}"
122
+ end
123
+
124
+ MASKS = [ 0x36, 0x5c, ]
125
+ IMASK, OMASK = *MASKS
126
+
127
+ def hmac_md5 text, key
128
+ key = Digest::MD5.digest key if key.length > 64
129
+ nulls = [ 0]*64
130
+ k_ip, k_op = *MASKS.map { |m|
131
+ (nulls.zip key.bytes).map { |n,k| ((k||n) ^ m).chr }.join
132
+ }
133
+ Digest::MD5.hexdigest k_op + (Digest::MD5.digest k_ip + text)
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+