hermeneutics 1.8

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