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.
@@ -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
+