rumbster 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.autotest +1 -0
  2. data/Gemfile +11 -0
  3. data/Gemfile.lock +34 -0
  4. data/LICENSE.txt +202 -0
  5. data/README.rdoc +50 -0
  6. data/Rakefile +50 -8
  7. data/VERSION +1 -0
  8. data/lib/message_observers.rb +3 -3
  9. data/lib/rumbster.rb +0 -0
  10. data/lib/smtp_protocol.rb +7 -7
  11. data/lib/smtp_states.rb +24 -58
  12. data/rumbster.gemspec +70 -0
  13. data/test/{message_observers_test.rb → test_message_observers.rb} +1 -1
  14. data/test/{rumbster_test.rb → test_rumbster.rb} +1 -1
  15. data/test/{smtp_protocol_test.rb → test_smtp_protocol.rb} +2 -19
  16. data/test/{smtp_states_test.rb → test_smtp_states.rb} +31 -87
  17. metadata +128 -76
  18. data/COPYING +0 -515
  19. data/README +0 -56
  20. data/vendor/tmail.rb +0 -4
  21. data/vendor/tmail/.cvsignore +0 -3
  22. data/vendor/tmail/Makefile +0 -19
  23. data/vendor/tmail/address.rb +0 -222
  24. data/vendor/tmail/base64.rb +0 -52
  25. data/vendor/tmail/compat.rb +0 -39
  26. data/vendor/tmail/config.rb +0 -50
  27. data/vendor/tmail/encode.rb +0 -447
  28. data/vendor/tmail/header.rb +0 -895
  29. data/vendor/tmail/info.rb +0 -14
  30. data/vendor/tmail/loader.rb +0 -1
  31. data/vendor/tmail/mail.rb +0 -869
  32. data/vendor/tmail/mailbox.rb +0 -386
  33. data/vendor/tmail/mbox.rb +0 -1
  34. data/vendor/tmail/net.rb +0 -260
  35. data/vendor/tmail/obsolete.rb +0 -122
  36. data/vendor/tmail/parser.rb +0 -1475
  37. data/vendor/tmail/parser.y +0 -372
  38. data/vendor/tmail/port.rb +0 -356
  39. data/vendor/tmail/scanner.rb +0 -22
  40. data/vendor/tmail/scanner_r.rb +0 -243
  41. data/vendor/tmail/stringio.rb +0 -256
  42. data/vendor/tmail/textutils.rb +0 -197
  43. data/vendor/tmail/tmail.rb +0 -1
  44. data/vendor/tmail/utils.rb +0 -23
data/vendor/tmail/info.rb DELETED
@@ -1,14 +0,0 @@
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
@@ -1 +0,0 @@
1
- require 'tmail/mailbox'
data/vendor/tmail/mail.rb DELETED
@@ -1,869 +0,0 @@
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