hermeneutics 1.10 → 1.13
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 +97 -62
- data/lib/hermeneutics/addrs.rb +6 -4
- data/lib/hermeneutics/boxes.rb +134 -119
- data/lib/hermeneutics/cgi.rb +210 -85
- 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 +16 -9
- data/lib/hermeneutics/contents.rb +10 -0
- data/lib/hermeneutics/css.rb +14 -10
- data/lib/hermeneutics/escape.rb +31 -34
- data/lib/hermeneutics/html.rb +38 -60
- data/lib/hermeneutics/mail.rb +248 -63
- data/lib/hermeneutics/message.rb +245 -254
- data/lib/hermeneutics/tags.rb +4 -0
- data/lib/hermeneutics/types.rb +8 -7
- data/lib/hermeneutics/version.rb +3 -4
- metadata +27 -6
- data/lib/hermeneutics/cli/pop.rb +0 -102
- data/lib/hermeneutics/transports.rb +0 -230
@@ -0,0 +1,245 @@
|
|
1
|
+
#
|
2
|
+
# hermeneutics/cli/imap/parser.rb -- Parsing IMAP responses
|
3
|
+
#
|
4
|
+
|
5
|
+
require "supplement"
|
6
|
+
|
7
|
+
require "hermeneutics/cli/imap/utf7imap"
|
8
|
+
|
9
|
+
|
10
|
+
module Hermeneutics
|
11
|
+
|
12
|
+
module Cli
|
13
|
+
|
14
|
+
module ImapTools
|
15
|
+
|
16
|
+
class StringReader
|
17
|
+
def initialize src
|
18
|
+
@src = src.lines
|
19
|
+
@src.each { |l| l.chomp! }
|
20
|
+
end
|
21
|
+
def readline
|
22
|
+
@src.shift
|
23
|
+
end
|
24
|
+
def eof? ; @src.empty? ; end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
class Parser
|
29
|
+
|
30
|
+
RE = %r/\A\s*(?:
|
31
|
+
(\()|
|
32
|
+
(\))|
|
33
|
+
("(?:[^\\"]|\\.)*")|
|
34
|
+
(\{\d+\}\z)|
|
35
|
+
((?:\[[^\]]*\]|[^ \t)])+)|
|
36
|
+
)/x
|
37
|
+
|
38
|
+
class <<self
|
39
|
+
|
40
|
+
def run input
|
41
|
+
p = new
|
42
|
+
l = input.readline
|
43
|
+
loop do
|
44
|
+
if l.empty? then
|
45
|
+
break if p.closed?
|
46
|
+
l = input.readline
|
47
|
+
end
|
48
|
+
l.slice! RE
|
49
|
+
case
|
50
|
+
when $1 then
|
51
|
+
p.step_in
|
52
|
+
when $2 then
|
53
|
+
p.step_out
|
54
|
+
when $3 then
|
55
|
+
r = UTF7.decode $3
|
56
|
+
p.add r
|
57
|
+
when $4 then
|
58
|
+
n = $4[1,$4.length-2].to_i
|
59
|
+
r = ""
|
60
|
+
while n > 0 do
|
61
|
+
l = input.readline
|
62
|
+
l or raise "No more data after {#$4}"
|
63
|
+
m = l.length
|
64
|
+
if n <= m then
|
65
|
+
r << (l.slice! 0, n)
|
66
|
+
else
|
67
|
+
r << l << "\n"
|
68
|
+
l.clear
|
69
|
+
n -= 2
|
70
|
+
end
|
71
|
+
n -= m
|
72
|
+
end
|
73
|
+
p.add r
|
74
|
+
when $5 then
|
75
|
+
r = $5.nil_if "NIL"
|
76
|
+
r = UTF7.decode r if r
|
77
|
+
p.add r
|
78
|
+
else
|
79
|
+
raise "Error reading '#$''"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
p
|
83
|
+
end
|
84
|
+
|
85
|
+
def compile input, compiler
|
86
|
+
p = run input
|
87
|
+
p.walk compiler
|
88
|
+
compiler.result
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
def initialize
|
94
|
+
@list = []
|
95
|
+
end
|
96
|
+
|
97
|
+
def step_in
|
98
|
+
if @sub then
|
99
|
+
@sub.step_in
|
100
|
+
else
|
101
|
+
@sub = self.class.new
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def step_out
|
106
|
+
if @sub.closed? then
|
107
|
+
@list.push @sub
|
108
|
+
@sub = nil
|
109
|
+
else
|
110
|
+
@sub.step_out
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def add token
|
115
|
+
if @sub then
|
116
|
+
@sub.add token
|
117
|
+
else
|
118
|
+
@list.push token
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def closed?
|
123
|
+
not @sub
|
124
|
+
end
|
125
|
+
|
126
|
+
def walk compiler
|
127
|
+
closed? or raise "Object was not fully parsed. Rest: #@sub"
|
128
|
+
@list.each { |x|
|
129
|
+
case x
|
130
|
+
when self.class then compiler.step do x.walk compiler end
|
131
|
+
else compiler.add x
|
132
|
+
end
|
133
|
+
}
|
134
|
+
compiler.finish
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
class Data
|
141
|
+
|
142
|
+
class <<self
|
143
|
+
alias [] new
|
144
|
+
end
|
145
|
+
|
146
|
+
attr_reader :name, :params
|
147
|
+
|
148
|
+
def initialize name, *params
|
149
|
+
@name, @params = name, params
|
150
|
+
end
|
151
|
+
|
152
|
+
def stream_lines r, &block
|
153
|
+
r << " " << @name.to_s
|
154
|
+
add_to_stream r, @params, &block
|
155
|
+
yield [r]
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# If you think this is too complicated, then complain to
|
161
|
+
# the designers of IMAP.
|
162
|
+
#
|
163
|
+
def add_to_stream r, ary, &block
|
164
|
+
ary.each { |a|
|
165
|
+
r << " " unless @opened
|
166
|
+
@opened = false
|
167
|
+
case a
|
168
|
+
when Array then
|
169
|
+
r << "("
|
170
|
+
@opened = true
|
171
|
+
add_to_stream r, a, &block
|
172
|
+
r << ")"
|
173
|
+
@opened = false
|
174
|
+
else
|
175
|
+
a = a.to_s
|
176
|
+
s = a.notempty? ? (a.split /\r?\n/, -1) : [""]
|
177
|
+
l = s.length - 1
|
178
|
+
if l > 0 then
|
179
|
+
m = 0
|
180
|
+
s.each { |e| m += e.length }
|
181
|
+
m += l*2
|
182
|
+
r << "{#{m}}"
|
183
|
+
_ = yield [r]
|
184
|
+
r.clear
|
185
|
+
yield s
|
186
|
+
else
|
187
|
+
r << (UTF7.encode a).to_s
|
188
|
+
end
|
189
|
+
end
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
public
|
194
|
+
|
195
|
+
class Compiler
|
196
|
+
|
197
|
+
def initialize
|
198
|
+
@list = []
|
199
|
+
end
|
200
|
+
|
201
|
+
def result
|
202
|
+
Data.new @name, *@list
|
203
|
+
end
|
204
|
+
|
205
|
+
def step
|
206
|
+
list_, @list = @list, []
|
207
|
+
sub_, @sub = @sub, true
|
208
|
+
yield
|
209
|
+
ensure
|
210
|
+
list_.push @list
|
211
|
+
@list, @sub = list_, sub_
|
212
|
+
end
|
213
|
+
|
214
|
+
def add x
|
215
|
+
if @sub then
|
216
|
+
@list.push x
|
217
|
+
else
|
218
|
+
if not @name then
|
219
|
+
is_name? x or raise "Not an item name: #{x}"
|
220
|
+
@name = x.to_sym
|
221
|
+
else
|
222
|
+
@list.push x
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def finish
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def is_name? x
|
233
|
+
x =~ /\A[A-Z]+\z/
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
#
|
2
|
+
# hermeneutics/cli/imap/utf7imap.rb -- IMAP's UTF-7
|
3
|
+
#
|
4
|
+
|
5
|
+
require "supplement"
|
6
|
+
|
7
|
+
module Hermeneutics
|
8
|
+
|
9
|
+
module Cli
|
10
|
+
|
11
|
+
module ImapTools
|
12
|
+
|
13
|
+
class UTF7
|
14
|
+
|
15
|
+
class <<self
|
16
|
+
|
17
|
+
def encode str
|
18
|
+
e = str.gsub /&|([^ -~]+)/ do
|
19
|
+
if $1 then
|
20
|
+
b64 = [($1.encode Encoding::UTF_16BE)].pack "m0"
|
21
|
+
b64.slice! %r/=+\z/
|
22
|
+
b64.tr! "/", ","
|
23
|
+
end
|
24
|
+
"&#{b64}-"
|
25
|
+
end
|
26
|
+
if e.empty? or e =~ %r/[ "]/ then
|
27
|
+
e = %Q["#{e.gsub /(["\\])/ do "\\#$1" end}"]
|
28
|
+
end
|
29
|
+
new e
|
30
|
+
end
|
31
|
+
|
32
|
+
def decode txt
|
33
|
+
(new txt).decode
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize txt
|
39
|
+
@txt = txt
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s ; @txt ; end
|
43
|
+
|
44
|
+
def decode
|
45
|
+
t = @txt
|
46
|
+
if t =~ /\A"(.*)"\z/ then
|
47
|
+
t = $1
|
48
|
+
t.gsub! /\\(.)/ do $1 end
|
49
|
+
end
|
50
|
+
t.gsub /&(.*?)-/ do
|
51
|
+
if $1.empty? then
|
52
|
+
"&"
|
53
|
+
else
|
54
|
+
r = $1
|
55
|
+
r.tr! ",", "/"
|
56
|
+
f = -r.length % 4
|
57
|
+
if f.nonzero? then
|
58
|
+
r << "=" * f
|
59
|
+
end
|
60
|
+
r, = r.unpack "m"
|
61
|
+
r.force_encoding Encoding::UTF_16BE
|
62
|
+
r.encode! Encoding.default_external
|
63
|
+
r
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
@@ -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
|
+
|