rumbster 1.0.0

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,14 @@
1
+ #
2
+ # info.rb
3
+ #
4
+ # Copyright (c) 1998-2004 Minero Aoki
5
+ #
6
+ # This program is free software.
7
+ # You can distribute/modify this program under the terms of
8
+ # the GNU Lesser General Public License version 2.1.
9
+ #
10
+
11
+ module TMail
12
+ Version = '0.10.8'
13
+ Copyright = 'Copyright (c) 1998-2004 Minero Aoki'
14
+ end
@@ -0,0 +1 @@
1
+ require 'tmail/mailbox'
@@ -0,0 +1,869 @@
1
+ #
2
+ # mail.rb
3
+ #
4
+ # Copyright (c) 1998-2004 Minero Aoki
5
+ #
6
+ # This program is free software.
7
+ # You can distribute/modify this program under the terms of
8
+ # the GNU Lesser General Public License version 2.1.
9
+ #
10
+
11
+ require 'tmail/encode'
12
+ require 'tmail/header'
13
+ require 'tmail/port'
14
+ require 'tmail/config'
15
+ require 'tmail/textutils'
16
+
17
+ module TMail
18
+
19
+ class BadMessage < StandardError; end
20
+
21
+
22
+ class Mail
23
+
24
+ def Mail.load(fname)
25
+ new(FilePort.new(fname))
26
+ end
27
+
28
+ def Mail.parse(str)
29
+ new(StringPort.new(str))
30
+ end
31
+
32
+ def initialize(port = nil, conf = DEFAULT_CONFIG)
33
+ @port = port || StringPort.new
34
+ @config = Config.to_config(conf)
35
+
36
+ @header = {}
37
+ @body_port = nil
38
+ @body_parsed = false
39
+ @epilogue = ''
40
+ @parts = []
41
+
42
+ @port.ropen {|f|
43
+ parse_header f
44
+ parse_body f unless @port.reproducible?
45
+ }
46
+ end
47
+
48
+ attr_reader :port
49
+
50
+ def inspect
51
+ "\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
52
+ end
53
+
54
+ #
55
+ # to_s interfaces
56
+ #
57
+
58
+ public
59
+
60
+ include StrategyInterface
61
+
62
+ def write_back(eol = "\n", charset = 'e')
63
+ parse_body
64
+ @port.wopen {|stream|
65
+ encoded eol, charset, stream
66
+ }
67
+ end
68
+
69
+ def accept(strategy)
70
+ with_multipart_encoding(strategy) {
71
+ ordered_each do |name, field|
72
+ next if field.empty?
73
+ strategy.header_name canonical(name)
74
+ field.accept strategy
75
+ strategy.puts
76
+ end
77
+ strategy.puts
78
+ body_port().ropen {|r|
79
+ strategy.write r.read
80
+ }
81
+ }
82
+ end
83
+
84
+ private
85
+
86
+ def canonical(name)
87
+ name.split(/-/).map {|s| s.capitalize }.join('-')
88
+ end
89
+
90
+ def with_multipart_encoding(strategy)
91
+ if parts().empty? # DO NOT USE @parts
92
+ yield
93
+
94
+ else
95
+ bound = (type_param('boundary') || ::TMail.new_boundary)
96
+ if @header.key?('content-type')
97
+ @header['content-type'].params['boundary'] = bound
98
+ else
99
+ store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
100
+ end
101
+
102
+ yield
103
+
104
+ parts().each do |m|
105
+ strategy.puts
106
+ strategy.puts '--' + bound
107
+ m.accept strategy
108
+ end
109
+ strategy.puts
110
+ strategy.puts '--' + bound + '--'
111
+ strategy.write epilogue()
112
+ end
113
+ end
114
+
115
+ ###
116
+ ### High level utilities
117
+ ###
118
+
119
+ public
120
+
121
+ def friendly_from(default = nil)
122
+ h = @header['from']
123
+ a, = h.addrs
124
+ return default unless a
125
+ return a.phrase if a.phrase
126
+ return h.comments.join(' ') unless h.comments.empty?
127
+ a.spec
128
+ end
129
+
130
+ def from_address(default = nil)
131
+ from([]).first || default
132
+ end
133
+
134
+ def destinations(default = nil)
135
+ result = to([]) + cc([]) + bcc([])
136
+ return default if result.empty?
137
+ result
138
+ end
139
+
140
+ def each_destination(&block)
141
+ destinations([]).each(&block)
142
+ end
143
+
144
+ alias each_dest each_destination
145
+
146
+ def reply_addresses(default = nil)
147
+ reply_to_addrs(nil) or from_addrs(nil) or default
148
+ end
149
+
150
+ def error_reply_addresses(default = nil)
151
+ if s = sender(nil)
152
+ [s]
153
+ else
154
+ from_addrs(default)
155
+ end
156
+ end
157
+
158
+ def base64_encode
159
+ store 'Content-Transfer-Encoding', 'Base64'
160
+ self.body = Base64.folding_encode(self.body)
161
+ end
162
+
163
+ def base64_decode
164
+ if /base64/i =~ self.transfer_encoding('')
165
+ store 'Content-Transfer-Encoding', '8bit'
166
+ self.body = Base64.decode(self.body, @config.strict_base64decode?)
167
+ end
168
+ end
169
+
170
+ def multipart?
171
+ main_type('').downcase == 'multipart'
172
+ end
173
+
174
+ def create_reply
175
+ mail = TMail::Mail.new
176
+ mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
177
+ mail.to_addrs = reply_addresses([])
178
+ mail.in_reply_to = [message_id(nil)].compact
179
+ mail.references = references([]) + [message_id(nil)].compact
180
+ mail.mime_version = '1.0'
181
+ mail
182
+ end
183
+
184
+ ###
185
+ ### Header access facades
186
+ ###
187
+
188
+ include TextUtils
189
+
190
+ public
191
+
192
+ def header_string(name, default = nil)
193
+ h = @header[name.downcase] or return default
194
+ h.to_s
195
+ end
196
+
197
+ #
198
+ # date time
199
+ #
200
+
201
+ def date(default = nil)
202
+ h = @header['date'] or return default
203
+ h.date
204
+ end
205
+
206
+ def date=(time)
207
+ if time
208
+ store 'Date', time2str(time)
209
+ else
210
+ @header.delete 'date'
211
+ end
212
+ time
213
+ end
214
+
215
+ def strftime(fmt, default = nil)
216
+ t = date or return default
217
+ t.strftime(fmt)
218
+ end
219
+
220
+ #
221
+ # destination
222
+ #
223
+
224
+ def to_addrs(default = nil)
225
+ h = @header['to'] or return default
226
+ h.addrs
227
+ end
228
+
229
+ def cc_addrs(default = nil)
230
+ h = @header['cc'] or return default
231
+ h.addrs
232
+ end
233
+
234
+ def bcc_addrs(default = nil)
235
+ h = @header['bcc'] or return default
236
+ h.addrs
237
+ end
238
+
239
+ def to_addrs=(arg)
240
+ set_addrfield 'to', arg
241
+ end
242
+
243
+ def cc_addrs=(arg)
244
+ set_addrfield 'cc', arg
245
+ end
246
+
247
+ def bcc_addrs=(arg)
248
+ set_addrfield 'bcc', arg
249
+ end
250
+
251
+ def to(default = nil)
252
+ addrs2specs(to_addrs(nil)) || default
253
+ end
254
+
255
+ def cc(default = nil)
256
+ addrs2specs(cc_addrs(nil)) || default
257
+ end
258
+
259
+ def bcc(default = nil)
260
+ addrs2specs(bcc_addrs(nil)) || default
261
+ end
262
+
263
+ def to=(*strs)
264
+ set_string_array_attr 'To', strs
265
+ end
266
+
267
+ def cc=(*strs)
268
+ set_string_array_attr 'Cc', strs
269
+ end
270
+
271
+ def bcc=(*strs)
272
+ set_string_array_attr 'Bcc', strs
273
+ end
274
+
275
+ #
276
+ # originator
277
+ #
278
+
279
+ def from_addrs(default = nil)
280
+ if h = @header['from']
281
+ h.addrs
282
+ else
283
+ default
284
+ end
285
+ end
286
+
287
+ def from_addrs=(arg)
288
+ set_addrfield 'from', arg
289
+ end
290
+
291
+ def from(default = nil)
292
+ addrs2specs(from_addrs(nil)) || default
293
+ end
294
+
295
+ def from=(*strs)
296
+ set_string_array_attr 'From', strs
297
+ end
298
+
299
+
300
+ def reply_to_addrs(default = nil)
301
+ h = @header['reply-to'] or return default
302
+ h.addrs
303
+ end
304
+
305
+ def reply_to_addrs=(arg)
306
+ set_addrfield 'reply-to', arg
307
+ end
308
+
309
+ def reply_to(default = nil)
310
+ addrs2specs(reply_to_addrs(nil)) || default
311
+ end
312
+
313
+ def reply_to=(*strs)
314
+ set_string_array_attr 'Reply-To', strs
315
+ end
316
+
317
+
318
+ def sender_addr(default = nil)
319
+ f = @header['sender'] or return default
320
+ f.addr || default
321
+ end
322
+
323
+ def sender_addr=(addr)
324
+ if addr
325
+ h = HeaderField.internal_new('sender', @config)
326
+ h.addr = addr
327
+ @header['sender'] = h
328
+ else
329
+ @header.delete 'sender'
330
+ end
331
+ addr
332
+ end
333
+
334
+ def sender(default)
335
+ f = @header['sender'] or return default
336
+ a = f.addr or return default
337
+ a.spec
338
+ end
339
+
340
+ def sender=(str)
341
+ set_string_attr 'Sender', str
342
+ end
343
+
344
+ #
345
+ # subject
346
+ #
347
+
348
+ def subject(default = nil)
349
+ h = @header['subject'] or return default
350
+ h.body
351
+ end
352
+
353
+ def subject=(str)
354
+ set_string_attr 'Subject', str
355
+ end
356
+
357
+ #
358
+ # identity & threading
359
+ #
360
+
361
+ def message_id(default = nil)
362
+ h = @header['message-id'] or return default
363
+ h.id || default
364
+ end
365
+
366
+ def message_id=(str)
367
+ set_string_attr 'Message-Id', str
368
+ end
369
+
370
+ def in_reply_to(default = nil)
371
+ h = @header['in-reply-to'] or return default
372
+ h.ids
373
+ end
374
+
375
+ def in_reply_to=(*idstrs)
376
+ set_string_array_attr 'In-Reply-To', idstrs
377
+ end
378
+
379
+ def references(default = nil)
380
+ h = @header['references'] or return default
381
+ h.refs
382
+ end
383
+
384
+ def references=(*strs)
385
+ set_string_array_attr 'References', strs
386
+ end
387
+
388
+ #
389
+ # MIME headers
390
+ #
391
+
392
+ def mime_version(default = nil)
393
+ h = @header['mime-version'] or return default
394
+ h.version || default
395
+ end
396
+
397
+ def mime_version=(m, opt = nil)
398
+ if opt
399
+ if h = @header['mime-version']
400
+ h.major = m
401
+ h.minor = opt
402
+ else
403
+ store 'Mime-Version', "#{m}.#{opt}"
404
+ end
405
+ else
406
+ store 'Mime-Version', m
407
+ end
408
+ m
409
+ end
410
+
411
+
412
+ def content_type(default = nil)
413
+ h = @header['content-type'] or return default
414
+ h.content_type || default
415
+ end
416
+
417
+ def main_type(default = nil)
418
+ h = @header['content-type'] or return default
419
+ h.main_type || default
420
+ end
421
+
422
+ def sub_type(default = nil)
423
+ h = @header['content-type'] or return default
424
+ h.sub_type || default
425
+ end
426
+
427
+ def set_content_type(str, sub = nil, param = nil)
428
+ if sub
429
+ main, sub = str, sub
430
+ else
431
+ main, sub = str.split(%r</>, 2)
432
+ raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
433
+ end
434
+ if h = @header['content-type']
435
+ h.main_type = main
436
+ h.sub_type = sub
437
+ h.params.clear
438
+ else
439
+ store 'Content-Type', "#{main}/#{sub}"
440
+ end
441
+ @header['content-type'].params.replace param if param
442
+
443
+ str
444
+ end
445
+
446
+ alias content_type= set_content_type
447
+
448
+ def type_param(name, default = nil)
449
+ h = @header['content-type'] or return default
450
+ h[name] || default
451
+ end
452
+
453
+ def charset(default = nil)
454
+ h = @header['content-type'] or return default
455
+ h['charset'] || default
456
+ end
457
+
458
+ def charset=(str)
459
+ if str
460
+ if h = @header[ 'content-type' ]
461
+ h['charset'] = str
462
+ else
463
+ store 'Content-Type', "text/plain; charset=#{str}"
464
+ end
465
+ end
466
+ str
467
+ end
468
+
469
+
470
+ def transfer_encoding(default = nil)
471
+ if h = @header['content-transfer-encoding']
472
+ h.encoding || default
473
+ else
474
+ default
475
+ end
476
+ end
477
+
478
+ def transfer_encoding=(str)
479
+ set_string_attr 'Content-Transfer-Encoding', str
480
+ end
481
+
482
+ alias encoding transfer_encoding
483
+ alias encoding= transfer_encoding=
484
+ alias content_transfer_encoding transfer_encoding
485
+ alias content_transfer_encoding= transfer_encoding=
486
+
487
+
488
+ def disposition(default = nil)
489
+ if h = @header['content-disposition']
490
+ h.disposition || default
491
+ else
492
+ default
493
+ end
494
+ end
495
+
496
+ alias content_disposition disposition
497
+
498
+ def set_disposition(pos, params = nil)
499
+ @header.delete 'content-disposition'
500
+ return pos unless pos
501
+ store('Content-Disposition', pos)
502
+ @header['content-disposition'].params.replace params if params
503
+ pos
504
+ end
505
+
506
+ alias disposition= set_disposition
507
+ alias set_content_disposition set_disposition
508
+ alias content_disposition= set_disposition
509
+
510
+ def disposition_param(name, default = nil)
511
+ if h = @header['content-disposition']
512
+ h[name] || default
513
+ else
514
+ default
515
+ end
516
+ end
517
+
518
+ #
519
+ # sub routines
520
+ #
521
+
522
+ def set_string_array_attr(key, strs)
523
+ strs.flatten!
524
+ if strs.empty?
525
+ @header.delete key.downcase
526
+ else
527
+ store key, strs.join(', ')
528
+ end
529
+ strs
530
+ end
531
+ private :set_string_array_attr
532
+
533
+ def set_string_attr(key, str)
534
+ if str
535
+ store key, str
536
+ else
537
+ @header.delete key.downcase
538
+ end
539
+ str
540
+ end
541
+ private :set_string_attr
542
+
543
+ def set_addrfield(name, arg)
544
+ if arg
545
+ h = HeaderField.internal_new(name, @config)
546
+ h.addrs.replace [arg].flatten
547
+ @header[name] = h
548
+ else
549
+ @header.delete name
550
+ end
551
+ arg
552
+ end
553
+ private :set_addrfield
554
+
555
+ def addrs2specs(addrs)
556
+ return nil unless addrs
557
+ list = addrs.map {|addr|
558
+ if addr.address_group?
559
+ then addr.map {|a| a.spec }
560
+ else addr.spec
561
+ end
562
+ }.flatten
563
+ return nil if list.empty?
564
+ list
565
+ end
566
+ private :addrs2specs
567
+
568
+ ###
569
+ ### Direct Header Access
570
+ ###
571
+
572
+ public
573
+
574
+ ALLOW_MULTIPLE = {
575
+ 'received' => true,
576
+ 'resent-date' => true,
577
+ 'resent-from' => true,
578
+ 'resent-sender' => true,
579
+ 'resent-to' => true,
580
+ 'resent-cc' => true,
581
+ 'resent-bcc' => true,
582
+ 'resent-message-id' => true,
583
+ 'comments' => true,
584
+ 'keywords' => true
585
+ }
586
+ USE_ARRAY = ALLOW_MULTIPLE
587
+
588
+ def header
589
+ @header.dup
590
+ end
591
+
592
+ def [](key)
593
+ @header[key.downcase]
594
+ end
595
+
596
+ alias fetch []
597
+
598
+ def []=(key, val)
599
+ dkey = key.downcase
600
+ if val.nil?
601
+ @header.delete dkey
602
+ return nil
603
+ end
604
+ case val
605
+ when String
606
+ header = new_hf(key, val)
607
+ when HeaderField
608
+ ;
609
+ when Array
610
+ raise BadMessage, "multiple #{key}: header fields exist"\
611
+ unless ALLOW_MULTIPLE.include?(dkey)
612
+ @header[dkey] = val
613
+ return val
614
+ else
615
+ header = new_hf(key, val.to_s)
616
+ end
617
+ if ALLOW_MULTIPLE.include? dkey
618
+ (@header[dkey] ||= []).push header
619
+ else
620
+ @header[dkey] = header
621
+ end
622
+
623
+ val
624
+ end
625
+
626
+ alias store []=
627
+
628
+ def each_header
629
+ @header.each do |key, val|
630
+ [val].flatten.each {|v| yield key, v }
631
+ end
632
+ end
633
+
634
+ alias each_pair each_header
635
+
636
+ def each_header_name(&block)
637
+ @header.each_key(&block)
638
+ end
639
+
640
+ alias each_key each_header_name
641
+
642
+ def each_field(&block)
643
+ @header.values.flatten.each(&block)
644
+ end
645
+
646
+ alias each_value each_field
647
+
648
+ FIELD_ORDER = %w(
649
+ return-path received
650
+ resent-date resent-from resent-sender resent-to
651
+ resent-cc resent-bcc resent-message-id
652
+ date from sender reply-to to cc bcc
653
+ message-id in-reply-to references
654
+ subject comments keywords
655
+ mime-version content-type content-transfer-encoding
656
+ content-disposition content-description
657
+ )
658
+
659
+ def ordered_each
660
+ list = @header.keys
661
+ FIELD_ORDER.each do |name|
662
+ if list.delete(name)
663
+ [@header[name]].flatten.each {|v| yield name, v }
664
+ end
665
+ end
666
+ list.each do |name|
667
+ [@header[name]].flatten.each {|v| yield name, v }
668
+ end
669
+ end
670
+
671
+ def clear
672
+ @header.clear
673
+ end
674
+
675
+ def delete(key)
676
+ @header.delete key.downcase
677
+ end
678
+
679
+ def delete_if
680
+ @header.delete_if {|key, val|
681
+ if val.is_a?(Array)
682
+ val.delete_if {|v| yield key, v }
683
+ val.empty?
684
+ else
685
+ yield key, val
686
+ end
687
+ }
688
+ end
689
+
690
+ def keys
691
+ @header.keys
692
+ end
693
+
694
+ def key?(key)
695
+ @header.key?(key.downcase)
696
+ end
697
+
698
+ def values_at(*args)
699
+ args.map {|k| @header[k.downcase] }.flatten
700
+ end
701
+
702
+ alias indexes values_at
703
+ alias indices values_at
704
+
705
+ private
706
+
707
+ def parse_header(f)
708
+ name = field = nil
709
+ unixfrom = nil
710
+
711
+ while line = f.gets
712
+ case line
713
+ when /\A[ \t]/ # continue from prev line
714
+ raise SyntaxError, 'mail is began by space' unless field
715
+ field << ' ' << line.strip
716
+ when /\A([^\: \t]+):\s*/ # new header line
717
+ add_hf name, field if field
718
+ name = $1
719
+ field = $' #.strip
720
+ when /\A\-*\s*\z/ # end of header
721
+ add_hf name, field if field
722
+ name = field = nil
723
+ break
724
+ when /\AFrom (\S+)/
725
+ unixfrom = $1
726
+ else
727
+ raise SyntaxError, "wrong mail header: '#{line.inspect}'"
728
+ end
729
+ end
730
+ add_hf name, field if name
731
+
732
+ if unixfrom
733
+ add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
734
+ end
735
+ end
736
+
737
+ def add_hf(name, field)
738
+ key = name.downcase
739
+ field = new_hf(name, field)
740
+
741
+ if ALLOW_MULTIPLE.include? key
742
+ (@header[key] ||= []).push field
743
+ else
744
+ @header[key] = field
745
+ end
746
+ end
747
+
748
+ def new_hf(name, field)
749
+ HeaderField.new(name, field, @config)
750
+ end
751
+
752
+ ###
753
+ ### Message Body
754
+ ###
755
+
756
+ public
757
+
758
+ def body_port
759
+ parse_body
760
+ @body_port
761
+ end
762
+
763
+ def each(&block)
764
+ body_port().ropen {|f| f.each(&block) }
765
+ end
766
+
767
+ def body
768
+ parse_body
769
+ @body_port.ropen {|f|
770
+ return f.read
771
+ }
772
+ end
773
+
774
+ def body=(str)
775
+ parse_body
776
+ @body_port.wopen {|f| f.write str }
777
+ str
778
+ end
779
+
780
+ alias preamble body
781
+ alias preamble= body=
782
+
783
+ def epilogue
784
+ parse_body
785
+ @epilogue.dup
786
+ end
787
+
788
+ def epilogue=(str)
789
+ parse_body
790
+ @epilogue = str
791
+ str
792
+ end
793
+
794
+ def parts
795
+ parse_body
796
+ @parts
797
+ end
798
+
799
+ def each_part(&block)
800
+ parts().each(&block)
801
+ end
802
+
803
+ private
804
+
805
+ def parse_body(f = nil)
806
+ return if @body_parsed
807
+ if f
808
+ parse_body_0 f
809
+ else
810
+ @port.ropen {|f|
811
+ skip_header f
812
+ parse_body_0 f
813
+ }
814
+ end
815
+ @body_parsed = true
816
+ end
817
+
818
+ def skip_header(f)
819
+ while line = f.gets
820
+ return if /\A[\r\n]*\z/ =~ line
821
+ end
822
+ end
823
+
824
+ def parse_body_0(f)
825
+ if multipart?
826
+ read_multipart f
827
+ else
828
+ read_singlepart f
829
+ end
830
+ end
831
+
832
+ def read_singlepart(f)
833
+ @body_port = @config.new_body_port(self)
834
+ @body_port.wopen {|w|
835
+ w.write f.read
836
+ }
837
+ end
838
+
839
+ def read_multipart(src)
840
+ bound = type_param('boundary')
841
+ return read_singlepart(src) unless bound
842
+ is_sep = /\A--#{Regexp.quote(bound)}(?:--)?[ \t]*(?:\n|\r\n|\r)/
843
+ lastbound = "--#{bound}--"
844
+
845
+ ports = [ @config.new_preamble_port(self) ]
846
+ begin
847
+ f = ports.last.wopen
848
+ while line = src.gets
849
+ if is_sep =~ line
850
+ f.close
851
+ break if line.strip == lastbound
852
+ ports.push @config.new_part_port(self)
853
+ f = ports.last.wopen
854
+ else
855
+ f << line
856
+ end
857
+ end
858
+ @epilogue = (src.read || '')
859
+ ensure
860
+ f.close if f and not f.closed?
861
+ end
862
+
863
+ @body_port = ports.shift
864
+ @parts = ports.map {|p| self.class.new(p, @config) }
865
+ end
866
+
867
+ end # class Mail
868
+
869
+ end # module TMail