hermeneutics 1.8
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.
- checksums.yaml +7 -0
- data/LICENSE +29 -0
- data/bin/hermesmail +262 -0
- data/etc/exim.conf +34 -0
- data/lib/hermeneutics/addrs.rb +687 -0
- data/lib/hermeneutics/boxes.rb +321 -0
- data/lib/hermeneutics/cgi.rb +253 -0
- data/lib/hermeneutics/cli/pop.rb +102 -0
- data/lib/hermeneutics/color.rb +275 -0
- data/lib/hermeneutics/contents.rb +351 -0
- data/lib/hermeneutics/css.rb +261 -0
- data/lib/hermeneutics/escape.rb +826 -0
- data/lib/hermeneutics/html.rb +462 -0
- data/lib/hermeneutics/mail.rb +105 -0
- data/lib/hermeneutics/message.rb +626 -0
- data/lib/hermeneutics/tags.rb +317 -0
- data/lib/hermeneutics/transports.rb +230 -0
- data/lib/hermeneutics/types.rb +137 -0
- data/lib/hermeneutics/version.rb +32 -0
- metadata +83 -0
@@ -0,0 +1,317 @@
|
|
1
|
+
#
|
2
|
+
# hermeneutics/tags.rb -- Parse HTML code
|
3
|
+
#
|
4
|
+
|
5
|
+
=begin rdoc
|
6
|
+
|
7
|
+
:section: Classes definied here
|
8
|
+
|
9
|
+
Hermeneutics::Parser Parses HTML source and builds a tree
|
10
|
+
|
11
|
+
Hermeneutics::Tags Compiles parsed code to a tag tree
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
|
16
|
+
require "hermeneutics/escape"
|
17
|
+
|
18
|
+
|
19
|
+
module Hermeneutics
|
20
|
+
|
21
|
+
# Parse a HTML file or string.
|
22
|
+
#
|
23
|
+
class Parser
|
24
|
+
|
25
|
+
class Error < StandardError ; end
|
26
|
+
|
27
|
+
ren = /[a-z_][a-z0-9_.-]*/i
|
28
|
+
|
29
|
+
RE_TAG = %r{\A\s*(#{ren}(?::#{ren})?)\s*(.*?)\s*(/)?>}mx
|
30
|
+
RE_INSTR = %r{\A\?\s*(#{ren})\s*(.*)\s*\?>}m
|
31
|
+
RE_COMMENT = %r{\A!--(.*?)-->}m
|
32
|
+
RE_CDATA = %r{\A!\[CDATA\[(.*?)\]\]>}m
|
33
|
+
RE_BANG = %r{\A!\s*([A-Z]+)\s*(.*?)>}m
|
34
|
+
RE_CMD = %r{\A!\s*(\[.*?\])\s*>}m
|
35
|
+
|
36
|
+
RE_ATTR = %r{\A(#{ren}(?::#{ren})?)(=)?}
|
37
|
+
|
38
|
+
Tok = Struct[ :type, :tag, :attrs, :data]
|
39
|
+
|
40
|
+
attr_reader :list
|
41
|
+
|
42
|
+
def initialize str, term = nil
|
43
|
+
@list = []
|
44
|
+
s = str
|
45
|
+
while s =~ /</ do
|
46
|
+
add_data $`
|
47
|
+
s = $'
|
48
|
+
e = case s
|
49
|
+
when %r{\A/\s*#{term}\s*>}im then
|
50
|
+
nil
|
51
|
+
when RE_TAG then
|
52
|
+
s = $'
|
53
|
+
t = Tok[ :tag, $1.downcase, (attrs $2),
|
54
|
+
(sub_parser s, $1, $3)]
|
55
|
+
s =~ %r{\A}
|
56
|
+
t
|
57
|
+
when RE_INSTR then Tok[ :instr, $1.downcase, (attrs $2), nil]
|
58
|
+
when RE_COMMENT then Tok[ :comm, nil, nil, $1 ]
|
59
|
+
when RE_CDATA then Tok[ nil, nil, nil, $1 ]
|
60
|
+
when RE_BANG then Tok[ :bang, $1, (attrl $2), nil]
|
61
|
+
when RE_CMD then Tok[ :cmd, $1, nil, nil]
|
62
|
+
else
|
63
|
+
raise Error, "Unclosed standalone tag <#{term}>."
|
64
|
+
end
|
65
|
+
s = $'
|
66
|
+
e or break
|
67
|
+
add_tok e
|
68
|
+
end
|
69
|
+
if term then
|
70
|
+
str.replace s
|
71
|
+
else
|
72
|
+
add_data s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def find_encoding
|
77
|
+
find_enc @list
|
78
|
+
end
|
79
|
+
|
80
|
+
def pretty_print
|
81
|
+
puts_tree @list, 0
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def sub_parser s, tag, close
|
87
|
+
self.class.new s, tag unless close
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_data str
|
91
|
+
if str.notempty? then
|
92
|
+
add_tok Tok[ nil, nil, nil, str]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_tok tok
|
97
|
+
if not tok.type and (l = @list.last) and not l.type then
|
98
|
+
l.data << tok.data
|
99
|
+
else
|
100
|
+
@list.push tok
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def attrs str
|
105
|
+
a = {}
|
106
|
+
while str.notempty? do
|
107
|
+
str.slice! RE_ATTR or
|
108
|
+
raise Error, "Illegal attribute specification: #{str}"
|
109
|
+
k = $1.downcase
|
110
|
+
a[ k] = if $2 then
|
111
|
+
attr_val str
|
112
|
+
else
|
113
|
+
str.lstrip!
|
114
|
+
k
|
115
|
+
end
|
116
|
+
end
|
117
|
+
a
|
118
|
+
end
|
119
|
+
|
120
|
+
def attrl str
|
121
|
+
a = []
|
122
|
+
while str.notempty? do
|
123
|
+
v = attr_val str
|
124
|
+
a.push v
|
125
|
+
end
|
126
|
+
a
|
127
|
+
end
|
128
|
+
|
129
|
+
def attr_val str
|
130
|
+
r = case str
|
131
|
+
when /\A"(.*?)"/m then $1
|
132
|
+
when /\A'(.*?)'/m then $1
|
133
|
+
when /\A\S+/ then $&
|
134
|
+
end
|
135
|
+
str.replace $'
|
136
|
+
str.lstrip!
|
137
|
+
r
|
138
|
+
end
|
139
|
+
|
140
|
+
def find_enc p
|
141
|
+
p.each { |e|
|
142
|
+
r = case e.type
|
143
|
+
when :tag then
|
144
|
+
case e.tag
|
145
|
+
when "html", "head" then
|
146
|
+
find_enc e.data.list
|
147
|
+
when "meta" then
|
148
|
+
e.attrs[ "charset"] || (
|
149
|
+
if e.attrs[ "http-equiv"] == "Content-Type" then
|
150
|
+
require "hermeneutics/contents"
|
151
|
+
c = Contents.parse e.attrs[ "content"]
|
152
|
+
c[ "charset"]
|
153
|
+
end
|
154
|
+
)
|
155
|
+
end
|
156
|
+
when :query then
|
157
|
+
e.attrs[ "encoding"]
|
158
|
+
end
|
159
|
+
return r if r
|
160
|
+
}
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
|
164
|
+
def puts_tree p, indent
|
165
|
+
p.each { |e|
|
166
|
+
print "%s[%s] %s " % [ " "*indent, e.type, e.tag, ]
|
167
|
+
r = case e.type
|
168
|
+
when :tag then puts ; puts_tree e.data.list, indent+1 if e.data
|
169
|
+
when nil then puts "%s%s" % [ " "*(indent+1), e.data.inspect, ]
|
170
|
+
else puts
|
171
|
+
end
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
# = Example
|
179
|
+
#
|
180
|
+
# This parses a table and outputs it as a CSV.
|
181
|
+
#
|
182
|
+
# t = Tags.compile "<table><tr><td> ... </table>", "iso-8859-15"
|
183
|
+
# t.table.each :tr do |row|
|
184
|
+
# if row.has? :th then
|
185
|
+
# l = row.map :th do |h| h.data end.join ";"
|
186
|
+
# else
|
187
|
+
# l = row.map :td do |c| c.data end.join ";"
|
188
|
+
# end
|
189
|
+
# puts l
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
class Tags
|
193
|
+
|
194
|
+
class <<self
|
195
|
+
|
196
|
+
def compile str, parser = nil
|
197
|
+
p = (parser||Parser).new str
|
198
|
+
enc = p.find_encoding||str.encoding
|
199
|
+
l = lex p, enc
|
200
|
+
new nil, nil, l
|
201
|
+
end
|
202
|
+
|
203
|
+
def lex parser, encoding = nil
|
204
|
+
r = []
|
205
|
+
while parser.list.any? do
|
206
|
+
e = parser.list.shift
|
207
|
+
case e.type
|
208
|
+
when :tag
|
209
|
+
a = {}
|
210
|
+
e.attrs.each { |k,v|
|
211
|
+
v.force_encoding encoding if encoding
|
212
|
+
a[ k.downcase.to_sym] = Entities.new.decode v
|
213
|
+
}
|
214
|
+
i = new e.tag, a
|
215
|
+
if e.data then
|
216
|
+
f = lex e.data, encoding
|
217
|
+
i.concat f
|
218
|
+
end
|
219
|
+
r.push i
|
220
|
+
when nil
|
221
|
+
d = e.data
|
222
|
+
d.force_encoding encoding if encoding
|
223
|
+
c = Entities.new.decode d
|
224
|
+
r.push c
|
225
|
+
when :instr then
|
226
|
+
when :comm then
|
227
|
+
when :bang then
|
228
|
+
when :cmd then
|
229
|
+
end
|
230
|
+
end
|
231
|
+
r
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
attr_reader :name, :attrs, :list
|
237
|
+
|
238
|
+
def initialize name, attrs = nil, *elems
|
239
|
+
@name = name.to_sym if name
|
240
|
+
@attrs = {}.update attrs if attrs
|
241
|
+
@list = []
|
242
|
+
@list.concat elems.flatten
|
243
|
+
end
|
244
|
+
|
245
|
+
def push elem
|
246
|
+
@list.push elem
|
247
|
+
end
|
248
|
+
|
249
|
+
def concat elems
|
250
|
+
@list.concat elems
|
251
|
+
end
|
252
|
+
|
253
|
+
def inspect
|
254
|
+
"<##@name [#{@list.length}]>"
|
255
|
+
end
|
256
|
+
|
257
|
+
def each t = nil
|
258
|
+
if t then
|
259
|
+
@list.each { |e|
|
260
|
+
yield e if Tags === e and e.name == t
|
261
|
+
}
|
262
|
+
else
|
263
|
+
@list.each { |e| yield e }
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def map t
|
268
|
+
@list.map { |e|
|
269
|
+
yield e if Tags === e and e.name == t
|
270
|
+
}.compact
|
271
|
+
end
|
272
|
+
|
273
|
+
def has_tag? t
|
274
|
+
@list.find { |e|
|
275
|
+
Tags === e and e.name == t
|
276
|
+
} and true
|
277
|
+
end
|
278
|
+
alias has? has_tag?
|
279
|
+
|
280
|
+
def tag t, n = nil
|
281
|
+
n ||= 0
|
282
|
+
@list.each { |e|
|
283
|
+
if Tags === e and e.name == t then
|
284
|
+
return e if n.zero?
|
285
|
+
n -= 1
|
286
|
+
end
|
287
|
+
}
|
288
|
+
nil
|
289
|
+
end
|
290
|
+
|
291
|
+
def method_missing sym, *args
|
292
|
+
(tag sym, *args) or super
|
293
|
+
rescue
|
294
|
+
super
|
295
|
+
end
|
296
|
+
|
297
|
+
def data
|
298
|
+
d = ""
|
299
|
+
gather_data self, d
|
300
|
+
d
|
301
|
+
end
|
302
|
+
|
303
|
+
private
|
304
|
+
|
305
|
+
def gather_data t, d
|
306
|
+
t.list.each { |e|
|
307
|
+
case e
|
308
|
+
when Tags then gather_data e, d
|
309
|
+
else d << e
|
310
|
+
end
|
311
|
+
}
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
317
|
+
|
@@ -0,0 +1,230 @@
|
|
1
|
+
#
|
2
|
+
# hermeneutics/transports.rb -- transporting mails
|
3
|
+
#
|
4
|
+
|
5
|
+
require "hermeneutics/mail"
|
6
|
+
require "hermeneutics/boxes"
|
7
|
+
|
8
|
+
require "supplement/locked"
|
9
|
+
|
10
|
+
|
11
|
+
module Hermeneutics
|
12
|
+
|
13
|
+
class Mail
|
14
|
+
|
15
|
+
SPOOLDIR = "/var/mail"
|
16
|
+
MAILDIR = "Mail"
|
17
|
+
SENDMAIL = "/usr/sbin/sendmail"
|
18
|
+
SYSDIR = ".hermeneutics"
|
19
|
+
|
20
|
+
LEVEL = {}
|
21
|
+
a = 0
|
22
|
+
LEVEL[ :ERR] = a += 1
|
23
|
+
LEVEL[ :INF] = a += 1
|
24
|
+
LEVEL[ :DBG] = a += 1
|
25
|
+
a = nil
|
26
|
+
|
27
|
+
class <<self
|
28
|
+
|
29
|
+
attr_accessor :spooldir, :spoolfile, :maildir, :sysdir, :default_format
|
30
|
+
attr_accessor :sendmail
|
31
|
+
attr_accessor :logfile, :loglevel
|
32
|
+
|
33
|
+
def box path = nil, default_format = nil
|
34
|
+
@cache ||= {}
|
35
|
+
@cache[ path] ||= find_box path, default_format
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def find_box path, default_format
|
41
|
+
b = case path
|
42
|
+
when Box then
|
43
|
+
path
|
44
|
+
when nil then
|
45
|
+
@spoolfile ||= getuser
|
46
|
+
@spooldir ||= SPOOLDIR
|
47
|
+
m = File.expand_path @spoolfile, @spooldir
|
48
|
+
MBox.new m
|
49
|
+
else
|
50
|
+
m = if path =~ /\A=/ then
|
51
|
+
File.join expand_maildir, $'
|
52
|
+
else
|
53
|
+
File.expand_path path, "~"
|
54
|
+
end
|
55
|
+
Box.find m, default_format||@default_format
|
56
|
+
end
|
57
|
+
b.exists? or b.create
|
58
|
+
b
|
59
|
+
end
|
60
|
+
|
61
|
+
public
|
62
|
+
|
63
|
+
def sendmail
|
64
|
+
@sendmail||SENDMAIL
|
65
|
+
end
|
66
|
+
|
67
|
+
def log type, *message
|
68
|
+
@logfile or return
|
69
|
+
return if LEVEL[ type] > LEVEL[ @loglevel].to_i
|
70
|
+
l = File.expand_path @logfile, expand_sysdir
|
71
|
+
LockedFile.open l, "a" do |log|
|
72
|
+
log.puts "[#{Time.new}] [#$$] [#{type}] #{message.join ' '}"
|
73
|
+
end
|
74
|
+
nil
|
75
|
+
rescue Errno::ENOENT
|
76
|
+
d = File.dirname l
|
77
|
+
Dir.mkdir! d and retry
|
78
|
+
end
|
79
|
+
|
80
|
+
def expand_maildir
|
81
|
+
File.expand_path @maildir||MAILDIR, "~"
|
82
|
+
end
|
83
|
+
|
84
|
+
def expand_sysdir
|
85
|
+
File.expand_path @sysdir||SYSDIR, expand_maildir
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def getuser
|
91
|
+
e = Etc.getpwuid Process.uid
|
92
|
+
e.name
|
93
|
+
rescue NameError
|
94
|
+
require "etc" and retry
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
# :call-seq:
|
100
|
+
# obj.save( path, default_format = nil) -> mb
|
101
|
+
#
|
102
|
+
# Save into local mailbox.
|
103
|
+
#
|
104
|
+
def save mailbox = nil, default_format = nil
|
105
|
+
b = cls.box mailbox, default_format
|
106
|
+
log :INF, "Delivering to", b.path
|
107
|
+
b.deliver self
|
108
|
+
end
|
109
|
+
|
110
|
+
# :call-seq:
|
111
|
+
# obj.pipe( cmd, *args) -> status
|
112
|
+
#
|
113
|
+
# Pipe into an external program. If a block is given, the programs
|
114
|
+
# output will be yielded there.
|
115
|
+
#
|
116
|
+
def pipe cmd, *args
|
117
|
+
log :INF, "Piping through:", cmd, *args
|
118
|
+
ri, wi = IO.pipe
|
119
|
+
ro, wo = IO.pipe
|
120
|
+
child = fork do
|
121
|
+
wi.close ; ro.close
|
122
|
+
$stdout.reopen wo ; wo.close
|
123
|
+
$stdin .reopen ri ; ri.close
|
124
|
+
exec cmd, *args
|
125
|
+
end
|
126
|
+
ri.close ; wo.close
|
127
|
+
t = Thread.new wi do |wi|
|
128
|
+
begin
|
129
|
+
wi.write to_s
|
130
|
+
ensure
|
131
|
+
wi.close
|
132
|
+
end
|
133
|
+
end
|
134
|
+
begin
|
135
|
+
r = ro.read
|
136
|
+
yield r if block_given?
|
137
|
+
ensure
|
138
|
+
ro.close
|
139
|
+
end
|
140
|
+
t.join
|
141
|
+
Process.wait child
|
142
|
+
$?.success? or
|
143
|
+
log :ERR, "Pipe failed with error code %d." % $?.exitstatus
|
144
|
+
$?
|
145
|
+
end
|
146
|
+
|
147
|
+
# :call-seq:
|
148
|
+
# obj.sendmail( *tos) -> status
|
149
|
+
#
|
150
|
+
# Send by sendmail; leave the +tos+ list empty to
|
151
|
+
# use Sendmail's -t option.
|
152
|
+
#
|
153
|
+
def sendmail *tos
|
154
|
+
if tos.empty? then
|
155
|
+
pipe cls.sendmail, "-t"
|
156
|
+
else
|
157
|
+
tos.flatten!
|
158
|
+
tos.map! { |t|
|
159
|
+
case t
|
160
|
+
when Addr then t.plain
|
161
|
+
else t.delete %q-,;"'<>(){}[]$&*?- # security
|
162
|
+
end
|
163
|
+
}
|
164
|
+
pipe cls.sendmail, *tos
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# :call-seq:
|
169
|
+
# obj.send!( smtp, *tos) -> response
|
170
|
+
#
|
171
|
+
# Send by SMTP.
|
172
|
+
#
|
173
|
+
# Be aware that <code>#send</code> without bang is a
|
174
|
+
# standard Ruby method.
|
175
|
+
#
|
176
|
+
def send! conn = nil, *tos
|
177
|
+
if tos.empty? then
|
178
|
+
tos = receivers.map { |t| t.plain }
|
179
|
+
else
|
180
|
+
tos.flatten!
|
181
|
+
end
|
182
|
+
f, m = true, ""
|
183
|
+
to_s.each_line { |l|
|
184
|
+
if f then
|
185
|
+
f = false
|
186
|
+
next if l =~ /^From /
|
187
|
+
end
|
188
|
+
m << l
|
189
|
+
}
|
190
|
+
open_smtp conn do |smtp|
|
191
|
+
log :INF, "Sending to", *tos
|
192
|
+
frs = headers.from.map { |f| f.plain }
|
193
|
+
smtp.send_message m, frs.first, tos
|
194
|
+
end
|
195
|
+
rescue NoMethodError
|
196
|
+
raise "Missing field: #{$!.name}."
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def net_smpt
|
202
|
+
Net::SMTP
|
203
|
+
rescue NameError
|
204
|
+
require "net/smtp" and retry
|
205
|
+
end
|
206
|
+
|
207
|
+
def open_smtp arg, &block
|
208
|
+
case arg
|
209
|
+
when String then h, p = arg.split ":"
|
210
|
+
when Array then h, p = *arg
|
211
|
+
when nil then h, p = "localhost", nil
|
212
|
+
else
|
213
|
+
if arg.respond_to? :send_message then
|
214
|
+
yield arg
|
215
|
+
return
|
216
|
+
else
|
217
|
+
h, p = arg.host, arg.port
|
218
|
+
end
|
219
|
+
end
|
220
|
+
net_smpt.start h, p, &block
|
221
|
+
end
|
222
|
+
|
223
|
+
def log level, *msg
|
224
|
+
cls.log level, *msg
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|