rumbster 1.0.0

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