hermeneutics 1.11 → 1.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README +11 -0
- data/bin/hermesmail +93 -58
- data/lib/hermeneutics/addrs.rb +6 -4
- data/lib/hermeneutics/boxes.rb +134 -119
- data/lib/hermeneutics/cgi.rb +16 -6
- data/lib/hermeneutics/cli/imap/commands.rb +283 -0
- data/lib/hermeneutics/cli/imap/parser.rb +245 -0
- data/lib/hermeneutics/cli/imap/utf7imap.rb +75 -0
- data/lib/hermeneutics/cli/imap.rb +240 -0
- data/lib/hermeneutics/cli/openssl.rb +11 -0
- data/lib/hermeneutics/cli/pop3.rb +257 -0
- data/lib/hermeneutics/cli/protocol.rb +141 -0
- data/lib/hermeneutics/cli/smtp.rb +218 -0
- data/lib/hermeneutics/color.rb +8 -8
- data/lib/hermeneutics/contents.rb +6 -0
- data/lib/hermeneutics/css.rb +10 -10
- data/lib/hermeneutics/escape.rb +30 -34
- data/lib/hermeneutics/html.rb +4 -4
- data/lib/hermeneutics/mail.rb +248 -63
- data/lib/hermeneutics/message.rb +238 -254
- data/lib/hermeneutics/types.rb +8 -7
- data/lib/hermeneutics/version.rb +3 -4
- metadata +26 -5
- data/lib/hermeneutics/cli/pop.rb +0 -102
- data/lib/hermeneutics/transports.rb +0 -230
@@ -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,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
|
+
|