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,105 @@
|
|
1
|
+
#
|
2
|
+
# hermeneutics/mail.rb -- A mail
|
3
|
+
#
|
4
|
+
|
5
|
+
require "hermeneutics/message"
|
6
|
+
|
7
|
+
module Hermeneutics
|
8
|
+
|
9
|
+
class Mail < Message
|
10
|
+
|
11
|
+
# :stopdoc:
|
12
|
+
class FromReader
|
13
|
+
class <<self
|
14
|
+
def open file
|
15
|
+
i = new file
|
16
|
+
yield i
|
17
|
+
end
|
18
|
+
private :new
|
19
|
+
end
|
20
|
+
attr_reader :from
|
21
|
+
def initialize file
|
22
|
+
@file = file
|
23
|
+
@file.eat_lines { |l|
|
24
|
+
l =~ /^From .*/ rescue nil
|
25
|
+
if $& then
|
26
|
+
@from = l
|
27
|
+
@from.chomp!
|
28
|
+
else
|
29
|
+
@first = l
|
30
|
+
end
|
31
|
+
break
|
32
|
+
}
|
33
|
+
end
|
34
|
+
def eat_lines &block
|
35
|
+
if @first then
|
36
|
+
yield @first
|
37
|
+
@first = nil
|
38
|
+
end
|
39
|
+
@file.eat_lines &block
|
40
|
+
end
|
41
|
+
end
|
42
|
+
# :startdoc:
|
43
|
+
|
44
|
+
class <<self
|
45
|
+
|
46
|
+
def parse input
|
47
|
+
FromReader.open input do |fr|
|
48
|
+
parse_hb fr do |h,b|
|
49
|
+
new fr.from, h, b
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def create
|
55
|
+
new nil, nil, nil
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize from, headers, body
|
61
|
+
super headers, body
|
62
|
+
@from = from
|
63
|
+
end
|
64
|
+
|
65
|
+
# String representation with "From " line.
|
66
|
+
# Mails reside in mbox files etc. and so have to end in a newline.
|
67
|
+
def to_s
|
68
|
+
set_unix_from
|
69
|
+
r = ""
|
70
|
+
r << @from << $/ << super
|
71
|
+
r.ends_with? $/ or r << $/
|
72
|
+
r
|
73
|
+
end
|
74
|
+
|
75
|
+
def receivers
|
76
|
+
addresses_of :to, :cc, :bcc
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def addresses_of *args
|
82
|
+
l = args.map { |f| @headers.field f }
|
83
|
+
AddrList.new *l
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_unix_from
|
87
|
+
return if @from
|
88
|
+
# Common MTA's will issue a proper "From" line; some MDA's
|
89
|
+
# won't. Then, build it using the "From:" header.
|
90
|
+
addr = nil
|
91
|
+
l = addresses_of :from, :return_path
|
92
|
+
# Prefer the non-local version if present.
|
93
|
+
l.each { |a|
|
94
|
+
if not addr or addr !~ /@/ then
|
95
|
+
addr = a
|
96
|
+
end
|
97
|
+
}
|
98
|
+
addr or raise ArgumentError, "No From: field present."
|
99
|
+
@from = "From #{addr.plain} #{Time.now.gmtime.asctime}"
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
@@ -0,0 +1,626 @@
|
|
1
|
+
#
|
2
|
+
# hermeneutics/message.rb -- a message as in mails or in HTTP communication
|
3
|
+
#
|
4
|
+
|
5
|
+
require "hermeneutics/types"
|
6
|
+
require "hermeneutics/contents"
|
7
|
+
require "hermeneutics/addrs"
|
8
|
+
|
9
|
+
|
10
|
+
class NilClass
|
11
|
+
def eat_lines
|
12
|
+
end
|
13
|
+
def rewind
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class String
|
17
|
+
def eat_lines
|
18
|
+
@pos ||= 0
|
19
|
+
while @pos < length do
|
20
|
+
p = index /.*\n?/, @pos
|
21
|
+
l = $&.length
|
22
|
+
begin
|
23
|
+
yield self[ @pos, l]
|
24
|
+
ensure
|
25
|
+
@pos += l
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
def rewind
|
30
|
+
@pos = 0
|
31
|
+
end
|
32
|
+
end
|
33
|
+
class Array
|
34
|
+
def eat_lines &block
|
35
|
+
@pos ||= 0
|
36
|
+
while @pos < length do
|
37
|
+
begin
|
38
|
+
self[ @pos].eat_lines &block
|
39
|
+
ensure
|
40
|
+
@pos += 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
def rewind
|
45
|
+
each { |e| e.rewind }
|
46
|
+
@pos = 0
|
47
|
+
end
|
48
|
+
end
|
49
|
+
class IO
|
50
|
+
def eat_lines &block
|
51
|
+
each_line &block
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
def to_s
|
55
|
+
rewind
|
56
|
+
read
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
module Hermeneutics
|
62
|
+
|
63
|
+
class Multipart < Mime
|
64
|
+
|
65
|
+
MIME = /^multipart\//
|
66
|
+
|
67
|
+
class IllegalBoundary < StandardError ; end
|
68
|
+
class ParseError < StandardError ; end
|
69
|
+
|
70
|
+
# :stopdoc:
|
71
|
+
class PartFile
|
72
|
+
class <<self
|
73
|
+
def open file, sep
|
74
|
+
i = new file, sep
|
75
|
+
yield i
|
76
|
+
end
|
77
|
+
private :new
|
78
|
+
end
|
79
|
+
public
|
80
|
+
attr_reader :prolog, :epilog
|
81
|
+
def initialize file, sep
|
82
|
+
@file = file
|
83
|
+
@sep = /^--#{Regexp.quote sep}(--)?/
|
84
|
+
read_part
|
85
|
+
@prolog = norm_nl @a
|
86
|
+
end
|
87
|
+
def next_part
|
88
|
+
return if @epilog
|
89
|
+
read_part
|
90
|
+
@a.first.chomp!
|
91
|
+
true
|
92
|
+
end
|
93
|
+
def eat_lines
|
94
|
+
yield @a.pop while @a.any?
|
95
|
+
end
|
96
|
+
private
|
97
|
+
def read_part
|
98
|
+
@a = []
|
99
|
+
e = nil
|
100
|
+
@file.eat_lines { |l|
|
101
|
+
l =~ @sep rescue nil
|
102
|
+
if $& then
|
103
|
+
e = [ $'] if $1
|
104
|
+
@a.reverse!
|
105
|
+
return
|
106
|
+
end
|
107
|
+
@a.push l
|
108
|
+
}
|
109
|
+
raise ParseError, "Missing separator #@sep"
|
110
|
+
ensure
|
111
|
+
if e then
|
112
|
+
@file.eat_lines { |l| e.push l }
|
113
|
+
e.reverse!
|
114
|
+
@epilog = norm_nl e
|
115
|
+
end
|
116
|
+
end
|
117
|
+
def norm_nl a
|
118
|
+
r = ""
|
119
|
+
while a.any? do
|
120
|
+
l = a.pop
|
121
|
+
l.chomp! and l << $/
|
122
|
+
r << l
|
123
|
+
end
|
124
|
+
r
|
125
|
+
end
|
126
|
+
end
|
127
|
+
# :startdoc:
|
128
|
+
|
129
|
+
public
|
130
|
+
|
131
|
+
class <<self
|
132
|
+
|
133
|
+
def parse input, parameters
|
134
|
+
b = parameters[ :boundary]
|
135
|
+
b or raise ParseError, "Missing boundary parameter."
|
136
|
+
PartFile.open input, b do |partfile|
|
137
|
+
list = []
|
138
|
+
while partfile.next_part do
|
139
|
+
m = Message.parse partfile
|
140
|
+
list.push m
|
141
|
+
end
|
142
|
+
new b, partfile.prolog, list, partfile.epilog
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
BOUNDARY_CHARS_STD = [ [*"0".."9"], [*"A".."Z"], [*"a".."z"]].join
|
149
|
+
BOUNDARY_CHARS = BOUNDARY_CHARS_STD + "+_./:=-" # "'()+_,-./:=?"
|
150
|
+
|
151
|
+
attr_reader :boundary, :prolog, :list, :epilog
|
152
|
+
|
153
|
+
def initialize boundary, prolog, list, epilog
|
154
|
+
@boundary = boundary.notempty?
|
155
|
+
@prolog, @list, @epilog = prolog, list, epilog
|
156
|
+
end
|
157
|
+
|
158
|
+
def boundary!
|
159
|
+
b = BOUNDARY_CHARS_STD.length
|
160
|
+
r = Time.now.strftime "%Y%m%d%H%M%S."
|
161
|
+
16.times { r << BOUNDARY_CHARS_STD[ (rand b)].chr }
|
162
|
+
@boundary = r
|
163
|
+
end
|
164
|
+
|
165
|
+
def inspect
|
166
|
+
r = ""
|
167
|
+
r << "#<#{cls}:"
|
168
|
+
r << "0x%x" % (object_id<<1)
|
169
|
+
r << " n=#{@list.length}"
|
170
|
+
r << ">"
|
171
|
+
end
|
172
|
+
|
173
|
+
def to_s
|
174
|
+
@boundary or raise IllegalBoundary
|
175
|
+
r = ""
|
176
|
+
splitter = "--#@boundary"
|
177
|
+
re = /#{Regexp.quote @boundary}/
|
178
|
+
@prolog =~ re and raise IllegalBoundary
|
179
|
+
r << @prolog
|
180
|
+
@list.each { |p|
|
181
|
+
s = p.to_s
|
182
|
+
s =~ re rescue nil
|
183
|
+
$& and raise IllegalBoundary
|
184
|
+
r << splitter << $/ << s << $/
|
185
|
+
}
|
186
|
+
@epilog =~ re and raise IllegalBoundary
|
187
|
+
r << splitter << "--" << @epilog
|
188
|
+
rescue IllegalBoundary
|
189
|
+
boundary!
|
190
|
+
retry
|
191
|
+
end
|
192
|
+
|
193
|
+
def [] num
|
194
|
+
@list[ num]
|
195
|
+
end
|
196
|
+
|
197
|
+
def each &block
|
198
|
+
@list.each &block
|
199
|
+
end
|
200
|
+
|
201
|
+
def length ; @list.length ; end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
|
206
|
+
class Message < Mime
|
207
|
+
|
208
|
+
MIME = "message/rfc822"
|
209
|
+
|
210
|
+
class ParseError < StandardError ; end
|
211
|
+
|
212
|
+
class Headers
|
213
|
+
|
214
|
+
class Entry
|
215
|
+
LINE_LENGTH = 78
|
216
|
+
INDENT = " "
|
217
|
+
class <<self
|
218
|
+
private :new
|
219
|
+
def parse str
|
220
|
+
str =~ /:\s*/ or
|
221
|
+
raise ParseError, "Header line without a colon: #{str}"
|
222
|
+
data = $'
|
223
|
+
new $`, $&, data
|
224
|
+
end
|
225
|
+
def create name, *contents
|
226
|
+
name = build_name name
|
227
|
+
i = new name.to_s, ": ", nil
|
228
|
+
i.set *contents
|
229
|
+
end
|
230
|
+
def build_name name
|
231
|
+
n = name.to_s
|
232
|
+
unless n.equal? name then
|
233
|
+
n.gsub! /_/, "-"
|
234
|
+
n.gsub! /\b[a-z]/ do |c| c.upcase end
|
235
|
+
end
|
236
|
+
n
|
237
|
+
end
|
238
|
+
end
|
239
|
+
attr_reader :name, :sep, :data
|
240
|
+
def initialize name, sep, data
|
241
|
+
@name, @sep, @data, @contents = name, sep, data
|
242
|
+
end
|
243
|
+
def to_s
|
244
|
+
"#@name#@sep#@data"
|
245
|
+
end
|
246
|
+
def contents type
|
247
|
+
if type then
|
248
|
+
unless @contents and @contents.is_a? type then
|
249
|
+
@contents = type.parse @data
|
250
|
+
end
|
251
|
+
@contents
|
252
|
+
else
|
253
|
+
@data
|
254
|
+
end
|
255
|
+
end
|
256
|
+
def name_is? name
|
257
|
+
(@name.casecmp name).zero?
|
258
|
+
end
|
259
|
+
def set *contents
|
260
|
+
type, *args = *contents
|
261
|
+
d = case type
|
262
|
+
when Class then
|
263
|
+
@contents = type.new *args
|
264
|
+
case (e = @contents.encode)
|
265
|
+
when Array then e
|
266
|
+
when nil then []
|
267
|
+
else [ e]
|
268
|
+
end
|
269
|
+
when nil then
|
270
|
+
@contents = nil
|
271
|
+
split_args args
|
272
|
+
else
|
273
|
+
@contents = nil
|
274
|
+
split_args contents
|
275
|
+
end
|
276
|
+
@data = mk_lines d
|
277
|
+
self
|
278
|
+
end
|
279
|
+
def reset type
|
280
|
+
if type then
|
281
|
+
c = contents type
|
282
|
+
@data = mk_lines c.encode if c
|
283
|
+
end
|
284
|
+
self
|
285
|
+
end
|
286
|
+
private
|
287
|
+
def mk_lines strs
|
288
|
+
m = LINE_LENGTH - @name.length - @sep.length
|
289
|
+
data = ""
|
290
|
+
strs.each { |e|
|
291
|
+
unless data.empty? then
|
292
|
+
if 1 + e.length <= m then
|
293
|
+
data << " "
|
294
|
+
m -= 1
|
295
|
+
else
|
296
|
+
data << $/ << INDENT
|
297
|
+
m = LINE_LENGTH - INDENT.length
|
298
|
+
end
|
299
|
+
end
|
300
|
+
data << e
|
301
|
+
m -= e.length
|
302
|
+
}
|
303
|
+
data
|
304
|
+
end
|
305
|
+
def split_args ary
|
306
|
+
r = []
|
307
|
+
ary.each { |a|
|
308
|
+
r.concat case a
|
309
|
+
when Array then split_args a
|
310
|
+
else a.to_s.split
|
311
|
+
end
|
312
|
+
}
|
313
|
+
r
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
@types = {
|
318
|
+
"Content-Type" => ContentType,
|
319
|
+
"To" => AddrList,
|
320
|
+
"Cc" => AddrList,
|
321
|
+
"Bcc" => AddrList,
|
322
|
+
"From" => AddrList,
|
323
|
+
"Subject" => PlainText,
|
324
|
+
"Content-Disposition" => Contents,
|
325
|
+
"Sender" => AddrList,
|
326
|
+
"Content-Transfer-Encoding" => Contents,
|
327
|
+
"User-Agent" => PlainText,
|
328
|
+
"Date" => Timestamp,
|
329
|
+
"Delivery-Date" => Timestamp,
|
330
|
+
"Message-ID" => Id,
|
331
|
+
"List-ID" => Id,
|
332
|
+
"References" => IdList,
|
333
|
+
"In-Reply-To" => Id,
|
334
|
+
"Reply-To" => AddrList,
|
335
|
+
"Content-Length" => Count,
|
336
|
+
"Lines" => Count,
|
337
|
+
"Return-Path" => AddrList,
|
338
|
+
"Envelope-To" => AddrList,
|
339
|
+
"DKIM-Signature" => Dictionary,
|
340
|
+
"DomainKey-Signature" => Dictionary,
|
341
|
+
|
342
|
+
"Set-Cookie" => Dictionary,
|
343
|
+
"Cookie" => Dictionary,
|
344
|
+
}
|
345
|
+
|
346
|
+
class <<self
|
347
|
+
def set_field_type name, type
|
348
|
+
e = Entry.create name
|
349
|
+
if type then
|
350
|
+
@types[ e.name] = type
|
351
|
+
else
|
352
|
+
@types.delete e.name
|
353
|
+
end
|
354
|
+
end
|
355
|
+
def find_type entry
|
356
|
+
@types.each { |k,v|
|
357
|
+
return v if entry.name_is? k
|
358
|
+
}
|
359
|
+
nil
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
class <<self
|
364
|
+
private :new
|
365
|
+
def parse *list
|
366
|
+
list.flatten!
|
367
|
+
list.map! { |h| Entry.parse h }
|
368
|
+
new list
|
369
|
+
end
|
370
|
+
def create
|
371
|
+
new []
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def initialize list
|
376
|
+
@list = list
|
377
|
+
end
|
378
|
+
|
379
|
+
def length
|
380
|
+
@list.length
|
381
|
+
end
|
382
|
+
alias size length
|
383
|
+
|
384
|
+
def to_s
|
385
|
+
@list.map { |e| "#{e}#$/" }.join
|
386
|
+
end
|
387
|
+
|
388
|
+
def each
|
389
|
+
@list.each { |e|
|
390
|
+
type = Headers.find_type e
|
391
|
+
c = e.contents type
|
392
|
+
yield e.name, c
|
393
|
+
}
|
394
|
+
self
|
395
|
+
end
|
396
|
+
|
397
|
+
def raw name
|
398
|
+
e = find_entry name
|
399
|
+
e.data if e
|
400
|
+
end
|
401
|
+
|
402
|
+
def field name, type = nil
|
403
|
+
e = find_entry name
|
404
|
+
if e then
|
405
|
+
type ||= Headers.find_type e
|
406
|
+
e.contents type
|
407
|
+
end
|
408
|
+
end
|
409
|
+
def [] name, type = nil
|
410
|
+
case name
|
411
|
+
when Integer then raise "Not a field name: #{name}"
|
412
|
+
else field name, type
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def method_missing sym, *args
|
417
|
+
if args.empty? and not sym =~ /[!?=]\z/ then
|
418
|
+
field sym, *args
|
419
|
+
else
|
420
|
+
super
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
def add name, *contents
|
425
|
+
e = build_entry name, *contents
|
426
|
+
add_entry e
|
427
|
+
self
|
428
|
+
end
|
429
|
+
|
430
|
+
def replace name, *contents
|
431
|
+
e = build_entry name, *contents
|
432
|
+
remove_entries e
|
433
|
+
add_entry e
|
434
|
+
self
|
435
|
+
end
|
436
|
+
|
437
|
+
def remove name
|
438
|
+
e = Entry.create name
|
439
|
+
remove_entries e
|
440
|
+
self
|
441
|
+
end
|
442
|
+
alias delete remove
|
443
|
+
|
444
|
+
def recode name, type = nil
|
445
|
+
n = Entry.build_name name
|
446
|
+
@list.each { |e|
|
447
|
+
next unless e.name_is? n
|
448
|
+
type ||= Headers.find_type e
|
449
|
+
e.reset type
|
450
|
+
}
|
451
|
+
self
|
452
|
+
end
|
453
|
+
|
454
|
+
def inspect
|
455
|
+
r = ""
|
456
|
+
r << "#<#{cls}:"
|
457
|
+
r << "0x%x" % (object_id<<1)
|
458
|
+
r << " (#{length})"
|
459
|
+
r << ">"
|
460
|
+
end
|
461
|
+
|
462
|
+
private
|
463
|
+
|
464
|
+
def find_entry name
|
465
|
+
e = Entry.build_name name
|
466
|
+
@list.find { |x| x.name_is? e }
|
467
|
+
end
|
468
|
+
|
469
|
+
def build_entry name, *contents
|
470
|
+
e = Entry.create name
|
471
|
+
type, = *contents
|
472
|
+
case type
|
473
|
+
when Class then
|
474
|
+
e.set *contents
|
475
|
+
else
|
476
|
+
type = Headers.find_type e
|
477
|
+
e.set type, *contents
|
478
|
+
end
|
479
|
+
e
|
480
|
+
end
|
481
|
+
|
482
|
+
def add_entry entry
|
483
|
+
@list.unshift entry
|
484
|
+
end
|
485
|
+
|
486
|
+
def remove_entries entry
|
487
|
+
@list.reject! { |e| e.name_is? entry.name }
|
488
|
+
end
|
489
|
+
|
490
|
+
end
|
491
|
+
|
492
|
+
class <<self
|
493
|
+
|
494
|
+
private :new
|
495
|
+
|
496
|
+
def parse input, parameters = nil
|
497
|
+
parse_hb input do |h,b|
|
498
|
+
new h, b
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def create
|
503
|
+
new nil, nil
|
504
|
+
end
|
505
|
+
|
506
|
+
private
|
507
|
+
|
508
|
+
def parse_hb input
|
509
|
+
h = parse_headers input
|
510
|
+
c = h.content_type
|
511
|
+
b = c.parse_mime input if c
|
512
|
+
unless b then
|
513
|
+
b = ""
|
514
|
+
input.eat_lines { |l| b << l }
|
515
|
+
b
|
516
|
+
end
|
517
|
+
yield h, b
|
518
|
+
end
|
519
|
+
|
520
|
+
def parse_headers input
|
521
|
+
h = []
|
522
|
+
input.eat_lines { |l|
|
523
|
+
l.chomp!
|
524
|
+
case l
|
525
|
+
when /^$/ then
|
526
|
+
break
|
527
|
+
when /^\s+/ then
|
528
|
+
h.last or
|
529
|
+
raise ParseError, "First line may not be a continuation."
|
530
|
+
h.last << $/ << l
|
531
|
+
else
|
532
|
+
h.push l
|
533
|
+
end
|
534
|
+
}
|
535
|
+
Headers.parse h
|
536
|
+
end
|
537
|
+
|
538
|
+
end
|
539
|
+
|
540
|
+
attr_reader :headers, :body
|
541
|
+
|
542
|
+
def initialize headers, body
|
543
|
+
@headers, @body = headers, body
|
544
|
+
@headers ||= Headers.create
|
545
|
+
end
|
546
|
+
|
547
|
+
def method_missing sym, *args, &block
|
548
|
+
case sym
|
549
|
+
when /h_(.*)/, /header_(.*)/ then
|
550
|
+
@headers.field $1.to_sym, *args
|
551
|
+
else
|
552
|
+
@headers.field sym, *args or super
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
def [] name, type = nil
|
557
|
+
@headers[ name, type]
|
558
|
+
end
|
559
|
+
|
560
|
+
def is_multipart?
|
561
|
+
Multipart === @body
|
562
|
+
end
|
563
|
+
alias mp? is_multipart?
|
564
|
+
|
565
|
+
def inspect
|
566
|
+
r = ""
|
567
|
+
r << "#<#{cls}:"
|
568
|
+
r << "0x%x" % (object_id<<1)
|
569
|
+
r << " headers:#{@headers.length}"
|
570
|
+
r << " multipart" if is_multipart?
|
571
|
+
r << ">"
|
572
|
+
end
|
573
|
+
|
574
|
+
def to_s
|
575
|
+
r = ""
|
576
|
+
if is_multipart? then
|
577
|
+
c = @headers.field :content_type
|
578
|
+
u = @body.boundary
|
579
|
+
if c[ :boundary] != u then
|
580
|
+
@headers.replace :content_type, c.fulltype, :boundary => u
|
581
|
+
end
|
582
|
+
end
|
583
|
+
r << @headers.to_s << $/ << @body.to_s
|
584
|
+
r
|
585
|
+
end
|
586
|
+
|
587
|
+
def transfer_encoding
|
588
|
+
c = @headers[ :content_transfer_encoding]
|
589
|
+
c.caption if c
|
590
|
+
end
|
591
|
+
|
592
|
+
def body_decoded
|
593
|
+
r = case transfer_encoding
|
594
|
+
when "quoted-printable" then
|
595
|
+
(@body.unpack "M").join
|
596
|
+
when "base64" then
|
597
|
+
(@body.unpack "m").join
|
598
|
+
else
|
599
|
+
@body.new_string
|
600
|
+
end
|
601
|
+
if (c = @headers.content_type) and (s = c[ :charset]) then
|
602
|
+
r.force_encoding s
|
603
|
+
end
|
604
|
+
r
|
605
|
+
end
|
606
|
+
|
607
|
+
def body_text= body
|
608
|
+
body = body.to_s
|
609
|
+
@headers.replace :content_type, "text/plain", charset: body.encoding
|
610
|
+
@headers.replace :content_transfer_encoding, "quoted-printable"
|
611
|
+
@body = [ body].pack "M*"
|
612
|
+
end
|
613
|
+
|
614
|
+
def body_binary= body
|
615
|
+
@headers.replace :content_transfer_encoding, "base64"
|
616
|
+
@body = [ body].pack "m*"
|
617
|
+
end
|
618
|
+
|
619
|
+
def body= body
|
620
|
+
@body = body
|
621
|
+
end
|
622
|
+
|
623
|
+
end
|
624
|
+
|
625
|
+
end
|
626
|
+
|