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,321 @@
|
|
1
|
+
#
|
2
|
+
# hermeneutics/boxes.rb -- Mailboxes
|
3
|
+
#
|
4
|
+
|
5
|
+
=begin rdoc
|
6
|
+
|
7
|
+
:section: Classes definied here
|
8
|
+
|
9
|
+
Hermeneutics::Box is a general Mailbox.
|
10
|
+
|
11
|
+
Hermeneutics::MBox is the traditional mbox format (text file, separated by a
|
12
|
+
blank line).
|
13
|
+
|
14
|
+
Hermeneutics::Maildir is the maildir format.
|
15
|
+
|
16
|
+
|
17
|
+
=end
|
18
|
+
|
19
|
+
|
20
|
+
require "supplement"
|
21
|
+
require "supplement/locked"
|
22
|
+
require "date"
|
23
|
+
|
24
|
+
|
25
|
+
module Hermeneutics
|
26
|
+
|
27
|
+
# Mailboxes
|
28
|
+
class Box
|
29
|
+
|
30
|
+
@boxes = []
|
31
|
+
|
32
|
+
class <<self
|
33
|
+
|
34
|
+
# :call-seq:
|
35
|
+
# Box.find( path, default = nil) -> box
|
36
|
+
#
|
37
|
+
# Create a Box object (some subclass of Box), depending on
|
38
|
+
# what type the box is found at <code>path</code>.
|
39
|
+
#
|
40
|
+
def find path, default_format = nil
|
41
|
+
b = @boxes.find { |b| b.check path }
|
42
|
+
b ||= default_format
|
43
|
+
b ||= if File.directory? path then
|
44
|
+
Maildir
|
45
|
+
elsif File.file? path then
|
46
|
+
MBox
|
47
|
+
else
|
48
|
+
# If still nothing was found use Postfix convention:
|
49
|
+
path =~ /\/$/ ? Maildir : MBox
|
50
|
+
end
|
51
|
+
b.new path
|
52
|
+
end
|
53
|
+
|
54
|
+
# :call-seq:
|
55
|
+
# Box.check( path) -> nil
|
56
|
+
#
|
57
|
+
# By default, subclass mailboxes do not exist. You should overwrite
|
58
|
+
# this behaviour.
|
59
|
+
#
|
60
|
+
def check path
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
attr_reader :boxes
|
65
|
+
def inherited cls
|
66
|
+
Box.boxes.push cls
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
# :call-seq:
|
72
|
+
# Box.new( path) -> box
|
73
|
+
#
|
74
|
+
# Instantiate a Box object, just store the <code>path</code>.
|
75
|
+
#
|
76
|
+
def initialize mailbox
|
77
|
+
@mailbox = mailbox
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_s ; path ; end
|
81
|
+
|
82
|
+
def path ; @mailbox ; end
|
83
|
+
|
84
|
+
# :call-seq:
|
85
|
+
# box.exists? -> true or false
|
86
|
+
#
|
87
|
+
# Test whether the <code>Box</code> exists.
|
88
|
+
#
|
89
|
+
def exists?
|
90
|
+
self.class.check @mailbox
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
class MBox < Box
|
96
|
+
|
97
|
+
RE_F = /^From\s+/ # :nodoc:
|
98
|
+
RE_N = /^$/ # :nodoc:
|
99
|
+
|
100
|
+
class <<self
|
101
|
+
|
102
|
+
# :call-seq:
|
103
|
+
# MBox.check( path) -> true or false
|
104
|
+
#
|
105
|
+
# Check whether path is a <code>MBox</code>.
|
106
|
+
#
|
107
|
+
def check path
|
108
|
+
if File.file? path then
|
109
|
+
File.open path, :encoding => Encoding::ASCII_8BIT do |f|
|
110
|
+
f.size.zero? or f.readline =~ RE_F
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
# :stopdoc:
|
118
|
+
class Region
|
119
|
+
class <<self
|
120
|
+
private :new
|
121
|
+
def open file, start, stop
|
122
|
+
t = file.tell
|
123
|
+
begin
|
124
|
+
i = new file, start, stop
|
125
|
+
yield i
|
126
|
+
ensure
|
127
|
+
file.seek t
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
def initialize file, start, stop
|
132
|
+
@file, @start, @stop = file, start, stop
|
133
|
+
rewind
|
134
|
+
end
|
135
|
+
def rewind ; @file.seek @start ; end
|
136
|
+
def read n = nil
|
137
|
+
m = @stop - @file.tell
|
138
|
+
n = m if not n or n > m
|
139
|
+
@file.read n
|
140
|
+
end
|
141
|
+
def to_s
|
142
|
+
rewind
|
143
|
+
read
|
144
|
+
end
|
145
|
+
def each_line
|
146
|
+
@file.each_line { |l|
|
147
|
+
break if @file.tell > @stop
|
148
|
+
yield l
|
149
|
+
}
|
150
|
+
end
|
151
|
+
alias eat_lines each_line
|
152
|
+
end
|
153
|
+
# :startdoc:
|
154
|
+
|
155
|
+
# :call-seq:
|
156
|
+
# mbox.create -> self
|
157
|
+
#
|
158
|
+
# Create the <code>MBox</code>.
|
159
|
+
#
|
160
|
+
def create
|
161
|
+
d = File.dirname @mailbox
|
162
|
+
Dir.mkdir! d
|
163
|
+
File.open @mailbox, File::CREAT do |f| end
|
164
|
+
self
|
165
|
+
end
|
166
|
+
|
167
|
+
# :call-seq:
|
168
|
+
# mbox.deliver( msg) -> nil
|
169
|
+
#
|
170
|
+
# Store the mail into the local <code>MBox</code>.
|
171
|
+
#
|
172
|
+
def deliver msg
|
173
|
+
pos = nil
|
174
|
+
LockedFile.open @mailbox, "r+", :encoding => Encoding::ASCII_8BIT do |f|
|
175
|
+
f.seek [ f.size - 4, 0].max
|
176
|
+
last = ""
|
177
|
+
f.read.each_line { |l| last = l }
|
178
|
+
f.puts unless last =~ /^$/
|
179
|
+
pos = f.size
|
180
|
+
m = msg.to_s
|
181
|
+
i = 1
|
182
|
+
while (i = m.index RE_F, i rescue nil) do m.insert i, ">" end
|
183
|
+
f.write m
|
184
|
+
f.puts
|
185
|
+
end
|
186
|
+
pos
|
187
|
+
end
|
188
|
+
|
189
|
+
# :call-seq:
|
190
|
+
# mbox.each { |mail| ... } -> nil
|
191
|
+
#
|
192
|
+
# Iterate through <code>MBox</code>.
|
193
|
+
#
|
194
|
+
def each &block
|
195
|
+
File.open @mailbox, :encoding => Encoding::ASCII_8BIT do |f|
|
196
|
+
m, e = nil, true
|
197
|
+
s, t = t, f.tell
|
198
|
+
f.each_line { |l|
|
199
|
+
s, t = t, f.tell
|
200
|
+
if is_from_line? l and e then
|
201
|
+
begin
|
202
|
+
m and Region.open f, m, e, &block
|
203
|
+
ensure
|
204
|
+
m, e = s, nil
|
205
|
+
end
|
206
|
+
else
|
207
|
+
m or raise "#@mailbox does not seem to be a mailbox."
|
208
|
+
e = l =~ RE_N && s
|
209
|
+
end
|
210
|
+
}
|
211
|
+
# Treat it gracefully when there is no empty last line.
|
212
|
+
e ||= f.tell
|
213
|
+
m and Region.open f, m, e, &block
|
214
|
+
end
|
215
|
+
end
|
216
|
+
include Enumerable
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def is_from_line? l
|
221
|
+
l =~ RE_F or return
|
222
|
+
addr, time = $'.split nil, 2
|
223
|
+
DateTime.parse time
|
224
|
+
addr =~ /@/
|
225
|
+
rescue ArgumentError, TypeError
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
class Maildir < Box
|
231
|
+
|
232
|
+
DIRS = %w(cur tmp new)
|
233
|
+
CUR, TMP, NEW = *DIRS
|
234
|
+
|
235
|
+
class <<self
|
236
|
+
|
237
|
+
# :call-seq:
|
238
|
+
# Maildir.check( path) -> true or false
|
239
|
+
#
|
240
|
+
# Check whether path is a <code>Maildir</code>.
|
241
|
+
#
|
242
|
+
def check mailbox
|
243
|
+
if File.directory? mailbox then
|
244
|
+
DIRS.each do |d|
|
245
|
+
s = File.join mailbox, d
|
246
|
+
File.directory? s or return false
|
247
|
+
end
|
248
|
+
true
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
# :call-seq:
|
255
|
+
# maildir.create -> self
|
256
|
+
#
|
257
|
+
# Create the <code>Maildir</code>.
|
258
|
+
#
|
259
|
+
def create
|
260
|
+
Dir.mkdir! @mailbox
|
261
|
+
DIRS.each do |d|
|
262
|
+
s = File.join @mailbox, d
|
263
|
+
Dir.mkdir s
|
264
|
+
end
|
265
|
+
self
|
266
|
+
end
|
267
|
+
|
268
|
+
# :call-seq:
|
269
|
+
# maildir.deliver( msg) -> nil
|
270
|
+
#
|
271
|
+
# Store the mail into the local <code>Maildir</code>.
|
272
|
+
#
|
273
|
+
def deliver msg
|
274
|
+
tmp = mkfilename TMP
|
275
|
+
File.open tmp, "w" do |f|
|
276
|
+
f.write msg
|
277
|
+
end
|
278
|
+
new = mkfilename NEW
|
279
|
+
File.rename tmp, new
|
280
|
+
new
|
281
|
+
end
|
282
|
+
|
283
|
+
# :call-seq:
|
284
|
+
# mbox.each { |mail| ... } -> nil
|
285
|
+
#
|
286
|
+
# Iterate through <code>MBox</code>.
|
287
|
+
#
|
288
|
+
def each
|
289
|
+
p = File.join @mailbox, CUR
|
290
|
+
d = Dir.new p
|
291
|
+
d.each { |f|
|
292
|
+
next if f.starts_with? "."
|
293
|
+
File.open f, :encoding => Encoding::ASCII_8BIT do |f|
|
294
|
+
yield f
|
295
|
+
end
|
296
|
+
}
|
297
|
+
end
|
298
|
+
include Enumerable
|
299
|
+
|
300
|
+
private
|
301
|
+
|
302
|
+
autoload :Socket, "socket"
|
303
|
+
|
304
|
+
def mkfilename d
|
305
|
+
dir = File.join @mailbox, d
|
306
|
+
c = 0
|
307
|
+
begin
|
308
|
+
n = "%.4f.%d_%d.%s" % [ Time.now.to_f, $$, c, Socket.gethostname]
|
309
|
+
path = File.join dir, n
|
310
|
+
File.open path, File::CREAT|File::EXCL do |f| end
|
311
|
+
path
|
312
|
+
rescue Errno::EEXIST
|
313
|
+
c += 1
|
314
|
+
retry
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
321
|
+
|
@@ -0,0 +1,253 @@
|
|
1
|
+
#
|
2
|
+
# hermeneutics/cgi.rb -- CGI responses
|
3
|
+
#
|
4
|
+
|
5
|
+
require "hermeneutics/escape"
|
6
|
+
require "hermeneutics/message"
|
7
|
+
require "hermeneutics/html"
|
8
|
+
|
9
|
+
|
10
|
+
module Hermeneutics
|
11
|
+
|
12
|
+
class Html
|
13
|
+
|
14
|
+
CONTENT_TYPE = "text/html"
|
15
|
+
|
16
|
+
attr_reader :cgi
|
17
|
+
|
18
|
+
def initialize cgi
|
19
|
+
@cgi = cgi
|
20
|
+
end
|
21
|
+
|
22
|
+
def form! **attrs, &block
|
23
|
+
attrs[ :action] = @cgi.fullpath attrs[ :action]
|
24
|
+
form **attrs, &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def href dest, params = nil, anchor = nil
|
28
|
+
@utx ||= URLText.new
|
29
|
+
dest = @cgi.fullpath dest
|
30
|
+
@utx.mkurl dest, params, anchor
|
31
|
+
end
|
32
|
+
|
33
|
+
def href! params = nil, anchor = nil
|
34
|
+
href nil, params, anchor
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Example:
|
41
|
+
#
|
42
|
+
# class MyCgi < Cgi
|
43
|
+
# def run
|
44
|
+
# p = parameters
|
45
|
+
# if p.empty? then
|
46
|
+
# location "/sorry.rb"
|
47
|
+
# else
|
48
|
+
# document MyHtml
|
49
|
+
# end
|
50
|
+
# rescue
|
51
|
+
# document MyErrorPage
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
# Cgi.execute
|
55
|
+
#
|
56
|
+
class Cgi
|
57
|
+
|
58
|
+
class <<self
|
59
|
+
attr_accessor :main
|
60
|
+
def inherited cls
|
61
|
+
Cgi.main = cls
|
62
|
+
end
|
63
|
+
def execute out = nil
|
64
|
+
(@main||self).new.execute out
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Overwrite this.
|
69
|
+
def run
|
70
|
+
document Html
|
71
|
+
end
|
72
|
+
|
73
|
+
def parameters inp = nil, &block
|
74
|
+
if block_given? then
|
75
|
+
case request_method
|
76
|
+
when "GET", "HEAD" then parse_query query_string, &block
|
77
|
+
when "POST" then parse_posted inp||$stdin, &block
|
78
|
+
else parse_input &block
|
79
|
+
end
|
80
|
+
else
|
81
|
+
p = {}
|
82
|
+
parameters do |k,v|
|
83
|
+
p[ k] = v
|
84
|
+
end
|
85
|
+
p
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
CGIENV = %w(content document gateway http query
|
90
|
+
remote request script server unique)
|
91
|
+
|
92
|
+
def method_missing sym, *args
|
93
|
+
if args.empty? and CGIENV.include? sym[ /\A(\w+?)_\w+\z/, 1] then
|
94
|
+
ENV[ sym.to_s.upcase]
|
95
|
+
else
|
96
|
+
super
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def https?
|
101
|
+
ENV[ "HTTPS"].notempty?
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def parse_query data, &block
|
107
|
+
URLText.decode_hash data, &block
|
108
|
+
end
|
109
|
+
|
110
|
+
def parse_posted inp, &block
|
111
|
+
data = inp.read
|
112
|
+
data.bytesize == content_length.to_i or
|
113
|
+
@warn = "Content length #{content_length} is wrong (#{data.bytesize})."
|
114
|
+
ct = ContentType.parse content_type
|
115
|
+
case ct.fulltype
|
116
|
+
when "application/x-www-form-urlencoded" then
|
117
|
+
parse_query data, &block
|
118
|
+
when "multipart/form-data" then
|
119
|
+
mp = Multipart.parse data, ct.hash
|
120
|
+
parse_multipart mp, &block
|
121
|
+
when "text/plain" then
|
122
|
+
# Suppose this is for testing purposes only.
|
123
|
+
mk_params data.lines, &block
|
124
|
+
else
|
125
|
+
parse_query data, &block
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse_multipart mp
|
130
|
+
mp.each { |part|
|
131
|
+
cd = part.headers.content_disposition
|
132
|
+
if cd.caption == "form-data" then
|
133
|
+
yield cd.name, part.body, **cd.hash
|
134
|
+
end
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
def mk_params l
|
139
|
+
l.each { |s|
|
140
|
+
k, v = s.split %r/=/
|
141
|
+
v ||= k
|
142
|
+
[k, v].each { |x| x.strip! }
|
143
|
+
yield k, v
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
def parse_input &block
|
148
|
+
if $*.any? then
|
149
|
+
l = $*
|
150
|
+
else
|
151
|
+
if $stdin.tty? then
|
152
|
+
$stderr.puts <<~EOT
|
153
|
+
Offline mode: Enter name=value pairs on standard input.
|
154
|
+
EOT
|
155
|
+
end
|
156
|
+
l = []
|
157
|
+
while (a = $stdin.gets) and a !~ /^$/ do
|
158
|
+
l.push a
|
159
|
+
end
|
160
|
+
end
|
161
|
+
ENV[ "SCRIPT_NAME"] = $0
|
162
|
+
mk_params l, &block
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
class Done < Exception
|
167
|
+
attr_reader :result
|
168
|
+
def initialize result
|
169
|
+
super nil
|
170
|
+
@result = result
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def done ct = nil
|
175
|
+
res = Message.create
|
176
|
+
yield res
|
177
|
+
d = Done.new res
|
178
|
+
raise d
|
179
|
+
end
|
180
|
+
|
181
|
+
public
|
182
|
+
|
183
|
+
def execute out = nil
|
184
|
+
@out ||= $stdout
|
185
|
+
begin
|
186
|
+
run
|
187
|
+
rescue
|
188
|
+
done { |res|
|
189
|
+
res.body = "#$! (#{$!.class})#$/"
|
190
|
+
$@.each { |a| res.body << "\t" << a << $/ }
|
191
|
+
res.headers.add :content_type,
|
192
|
+
"text/plain", charset: res.body.encoding
|
193
|
+
}
|
194
|
+
end
|
195
|
+
rescue Done
|
196
|
+
@out << $!.result.to_s
|
197
|
+
ensure
|
198
|
+
@out = nil
|
199
|
+
end
|
200
|
+
|
201
|
+
def document cls = Html, *args, &block
|
202
|
+
done { |res|
|
203
|
+
doc = cls.new self
|
204
|
+
res.body = ""
|
205
|
+
doc.generate res.body do
|
206
|
+
doc.document *args, &block
|
207
|
+
end
|
208
|
+
|
209
|
+
ct = if doc.respond_to? :content_type then doc.content_type
|
210
|
+
elsif cls.const_defined? :CONTENT_TYPE then doc.class::CONTENT_TYPE
|
211
|
+
end
|
212
|
+
ct and res.headers.add :content_type, ct,
|
213
|
+
charset: res.body.encoding||Encoding.default_external
|
214
|
+
if doc.respond_to? :cookies then
|
215
|
+
doc.cookies do |c|
|
216
|
+
res.headers.add :set_cookie, c
|
217
|
+
end
|
218
|
+
end
|
219
|
+
}
|
220
|
+
end
|
221
|
+
|
222
|
+
def location dest = nil, params = nil, anchor = nil
|
223
|
+
if Hash === dest then
|
224
|
+
dest, params, anchor = anchor, dest, params
|
225
|
+
end
|
226
|
+
utx = URLText.new mask_space: true
|
227
|
+
unless dest =~ %r{\A\w+://} then
|
228
|
+
dest = %Q'#{https? ? "https" : "http"}://#{http_host}#{fullpath dest}'
|
229
|
+
end
|
230
|
+
url = utx.mkurl dest, params, anchor
|
231
|
+
done { |res| res.headers.add "Location", url }
|
232
|
+
end
|
233
|
+
|
234
|
+
def fullpath dest
|
235
|
+
if dest then
|
236
|
+
dest =~ %r{\A/} || dest =~ /\.\w+\z/ ? dest : dest + ".rb"
|
237
|
+
else
|
238
|
+
File.basename script_name
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
if defined? MOD_RUBY then
|
244
|
+
# This has not been tested.
|
245
|
+
def query_string
|
246
|
+
Apache::request.args
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|