hermeneutics 1.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|