Project

General

Profile

Defect #8751 » header.rb

Drew Keller, 2011-08-08 22:23

 
1
#--
2
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
#
23
# Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24
# with permission of Minero Aoki.
25
#++
26

    
27
require 'tmail/encode'
28
require 'tmail/address'
29
require 'tmail/parser'
30
require 'tmail/config'
31
require 'tmail/utils'
32

    
33
#:startdoc:
34
module TMail
35

    
36
  # Provides methods to handle and manipulate headers in the email
37
  class HeaderField
38

    
39
    include TextUtils
40

    
41
    class << self
42

    
43
      alias newobj new
44

    
45
      def new( name, body, conf = DEFAULT_CONFIG )
46
        klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader
47
        klass.newobj body, conf
48
      end
49

    
50
      # Returns a HeaderField object matching the header you specify in the "name" param.
51
      # Requires an initialized TMail::Port to be passed in.
52
      #
53
      # The method searches the header of the Port you pass into it to find a match on
54
      # the header line you pass.  Once a match is found, it will unwrap the matching line
55
      # as needed to return an initialized HeaderField object.
56
      #
57
      # If you want to get the Envelope sender of the email object, pass in "EnvelopeSender",
58
      # if you want the From address of the email itself, pass in 'From'.
59
      #
60
      # This is because a mailbox doesn't have the : after the From that designates the
61
      # beginning of the envelope sender (which can be different to the from address of 
62
      # the email)
63
      #
64
      # Other fields can be passed as normal, "Reply-To", "Received" etc.
65
      #
66
      # Note: Change of behaviour in 1.2.1 => returns nil if it does not find the specified
67
      # header field, otherwise returns an instantiated object of the correct header class
68
      # 
69
      # For example:
70
      #   port = TMail::FilePort.new("/test/fixtures/raw_email_simple")
71
      #   h = TMail::HeaderField.new_from_port(port, "From")
72
      #   h.addrs.to_s #=> "Mikel Lindsaar <mikel@nowhere.com>"
73
      #   h = TMail::HeaderField.new_from_port(port, "EvelopeSender")
74
      #   h.addrs.to_s #=> "mike@anotherplace.com.au"
75
      #   h = TMail::HeaderField.new_from_port(port, "SomeWeirdHeaderField")
76
      #   h #=> nil
77
      def new_from_port( port, name, conf = DEFAULT_CONFIG )
78
        if name == "EnvelopeSender"
79
          name = "From"
80
          re = Regexp.new('\A(From) ', 'i')
81
        else
82
          re = Regexp.new('\A(' + Regexp.quote(name) + '):', 'i')
83
        end
84
        str = nil
85
        port.ropen {|f|
86
            f.each do |line|
87
              if m = re.match(line)            then str = m.post_match.strip
88
              elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
89
              elsif /\A-*\s*\z/ === line       then break
90
              elsif str                        then break
91
              end
92
            end
93
        }
94
        new(name, str, Config.to_config(conf)) if str
95
      end
96

    
97
      def internal_new( name, conf )
98
        FNAME_TO_CLASS[name].newobj('', conf, true)
99
      end
100

    
101
    end   # class << self
102

    
103
    def initialize( body, conf, intern = false )
104
      @body = body
105
      @config = conf
106

    
107
      @illegal = false
108
      @parsed = false
109
      
110
      if intern
111
        @parsed = true
112
        parse_init
113
      end
114
    end
115

    
116
    def inspect
117
      "#<#{self.class} #{@body.inspect}>"
118
    end
119

    
120
    def illegal?
121
      @illegal
122
    end
123

    
124
    def empty?
125
      ensure_parsed
126
      return true if @illegal
127
      isempty?
128
    end
129

    
130
    private
131

    
132
    def ensure_parsed
133
      return if @parsed
134
      @parsed = true
135
      parse
136
    end
137

    
138
    # defabstract parse
139
    # end
140

    
141
    def clear_parse_status
142
      @parsed = false
143
      @illegal = false
144
    end
145

    
146
    public
147

    
148
    def body
149
      ensure_parsed
150
      v = Decoder.new(s = '')
151
      do_accept v
152
      v.terminate
153
      s
154
    end
155

    
156
    def body=( str )
157
      @body = str
158
      clear_parse_status
159
    end
160

    
161
    include StrategyInterface
162

    
163
    def accept( strategy )
164
      ensure_parsed
165
      do_accept strategy
166
      strategy.terminate
167
    end
168

    
169
    # abstract do_accept
170

    
171
  end
172

    
173

    
174
  class UnstructuredHeader < HeaderField
175

    
176
    def body
177
      ensure_parsed
178
      @body
179
    end
180

    
181
    def body=( arg )
182
      ensure_parsed
183
      @body = arg
184
    end
185

    
186
    private
187

    
188
    def parse_init
189
    end
190

    
191
    def parse
192
      @body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
193
    end
194

    
195
    def isempty?
196
      not @body
197
    end
198

    
199
    def do_accept( strategy )
200
      strategy.text @body
201
    end
202

    
203
  end
204

    
205

    
206
  class StructuredHeader < HeaderField
207

    
208
    def comments
209
      ensure_parsed
210
      if @comments[0]
211
        [Decoder.decode(@comments[0])]
212
      else
213
        @comments
214
      end
215
    end
216

    
217
    private
218

    
219
    def parse
220
      save = nil
221

    
222
      begin
223
        parse_init
224
        do_parse
225
      rescue SyntaxError
226
        if not save and mime_encoded? @body
227
          save = @body
228
          @body = Decoder.decode(save)
229
          retry
230
        elsif save
231
          @body = save
232
        end
233

    
234
        @illegal = true
235
        raise if @config.strict_parse?
236
      end
237
    end
238

    
239
    def parse_init
240
      @comments = []
241
      init
242
    end
243

    
244
    def do_parse
245
      quote_boundary
246
      quote_unquoted_name
247
      quote_unquoted_bencode
248
      obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
249
      set obj if obj
250
    end
251

    
252
  end
253

    
254

    
255
  class DateTimeHeader < StructuredHeader
256

    
257
    PARSE_TYPE = :DATETIME
258

    
259
    def date
260
      ensure_parsed
261
      @date
262
    end
263

    
264
    def date=( arg )
265
      ensure_parsed
266
      @date = arg
267
    end
268

    
269
    private
270

    
271
    def init
272
      @date = nil
273
    end
274

    
275
    def set( t )
276
      @date = t
277
    end
278

    
279
    def isempty?
280
      not @date
281
    end
282

    
283
    def do_accept( strategy )
284
      strategy.meta time2str(@date)
285
    end
286

    
287
  end
288

    
289

    
290
  class AddressHeader < StructuredHeader
291

    
292
    PARSE_TYPE = :MADDRESS
293

    
294
    def addrs
295
      ensure_parsed
296
      @addrs
297
    end
298

    
299
    private
300

    
301
    def init
302
      @addrs = []
303
    end
304

    
305
    def set( a )
306
      @addrs = a
307
    end
308

    
309
    def isempty?
310
      @addrs.empty?
311
    end
312

    
313
    def do_accept( strategy )
314
      first = true
315
      @addrs.each do |a|
316
        if first
317
          first = false
318
        else
319
          strategy.meta ',' #strategy.puts_meta ','
320
          strategy.space
321
        end
322
        a.accept strategy
323
      end
324

    
325
      @comments.each do |c|
326
        strategy.space
327
        strategy.meta '('
328
        strategy.text c
329
        strategy.meta ')'
330
      end
331
    end
332

    
333
  end
334

    
335

    
336
  class ReturnPathHeader < AddressHeader
337

    
338
    PARSE_TYPE = :RETPATH
339

    
340
    def addr
341
      addrs()[0]
342
    end
343

    
344
    def spec
345
      a = addr() or return nil
346
      a.spec
347
    end
348

    
349
    def routes
350
      a = addr() or return nil
351
      a.routes
352
    end
353

    
354
    private
355

    
356
    def do_accept( strategy )
357
      a = addr()
358

    
359
      strategy.meta '<'
360
      unless a.routes.empty?
361
        strategy.meta a.routes.map {|i| '@' + i }.join(',')
362
        strategy.meta ':'
363
      end
364
      spec = a.spec
365
      strategy.meta spec if spec
366
      strategy.meta '>'
367
    end
368

    
369
  end
370

    
371

    
372
  class SingleAddressHeader < AddressHeader
373

    
374
    def addr
375
      addrs()[0]
376
    end
377

    
378
    private
379

    
380
    def do_accept( strategy )
381
      a = addr()
382
      a.accept strategy
383
      @comments.each do |c|
384
        strategy.space
385
        strategy.meta '('
386
        strategy.text c
387
        strategy.meta ')'
388
      end
389
    end
390

    
391
  end
392

    
393

    
394
  class MessageIdHeader < StructuredHeader
395

    
396
    def id
397
      ensure_parsed
398
      @id
399
    end
400

    
401
    def id=( arg )
402
      ensure_parsed
403
      @id = arg
404
    end
405

    
406
    private
407

    
408
    def init
409
      @id = nil
410
    end
411

    
412
    def isempty?
413
      not @id
414
    end
415

    
416
    def do_parse
417
      @id = @body.slice(MESSAGE_ID) or
418
              raise SyntaxError, "wrong Message-ID format: #{@body}"
419
    end
420

    
421
    def do_accept( strategy )
422
      strategy.meta @id
423
    end
424

    
425
  end
426

    
427

    
428
  class ReferencesHeader < StructuredHeader
429

    
430
    def refs
431
      ensure_parsed
432
      @refs
433
    end
434

    
435
    def each_id
436
      self.refs.each do |i|
437
        yield i if MESSAGE_ID === i
438
      end
439
    end
440

    
441
    def ids
442
      ensure_parsed
443
      @ids
444
    end
445

    
446
    def each_phrase
447
      self.refs.each do |i|
448
        yield i unless MESSAGE_ID === i
449
      end
450
    end
451

    
452
    def phrases
453
      ret = []
454
      each_phrase {|i| ret.push i }
455
      ret
456
    end
457

    
458
    private
459

    
460
    def init
461
      @refs = []
462
      @ids = []
463
    end
464

    
465
    def isempty?
466
      @ids.empty?
467
    end
468

    
469
    def do_parse
470
      str = @body
471
      while m = MESSAGE_ID.match(str)
472
        pre = m.pre_match.strip
473
        @refs.push pre unless pre.empty?
474
        @refs.push s = m[0]
475
        @ids.push s
476
        str = m.post_match
477
      end
478
      str = str.strip
479
      @refs.push str unless str.empty?
480
    end
481

    
482
    def do_accept( strategy )
483
      first = true
484
      @ids.each do |i|
485
        if first
486
          first = false
487
        else
488
          strategy.space
489
        end
490
        strategy.meta i
491
      end
492
    end
493

    
494
  end
495

    
496

    
497
  class ReceivedHeader < StructuredHeader
498

    
499
    PARSE_TYPE = :RECEIVED
500

    
501
    def from
502
      ensure_parsed
503
      @from
504
    end
505

    
506
    def from=( arg )
507
      ensure_parsed
508
      @from = arg
509
    end
510

    
511
    def by
512
      ensure_parsed
513
      @by
514
    end
515

    
516
    def by=( arg )
517
      ensure_parsed
518
      @by = arg
519
    end
520

    
521
    def via
522
      ensure_parsed
523
      @via
524
    end
525

    
526
    def via=( arg )
527
      ensure_parsed
528
      @via = arg
529
    end
530

    
531
    def with
532
      ensure_parsed
533
      @with
534
    end
535

    
536
    def id
537
      ensure_parsed
538
      @id
539
    end
540

    
541
    def id=( arg )
542
      ensure_parsed
543
      @id = arg
544
    end
545

    
546
    def _for
547
      ensure_parsed
548
      @_for
549
    end
550

    
551
    def _for=( arg )
552
      ensure_parsed
553
      @_for = arg
554
    end
555

    
556
    def date
557
      ensure_parsed
558
      @date
559
    end
560

    
561
    def date=( arg )
562
      ensure_parsed
563
      @date = arg
564
    end
565

    
566
    private
567

    
568
    def init
569
      @from = @by = @via = @with = @id = @_for = nil
570
      @with = []
571
      @date = nil
572
    end
573

    
574
    def set( args )
575
      @from, @by, @via, @with, @id, @_for, @date = *args
576
    end
577

    
578
    def isempty?
579
      @with.empty? and not (@from or @by or @via or @id or @_for or @date)
580
    end
581

    
582
    def do_accept( strategy )
583
      list = []
584
      list.push 'from '  + @from       if @from
585
      list.push 'by '    + @by         if @by
586
      list.push 'via '   + @via        if @via
587
      @with.each do |i|
588
        list.push 'with ' + i
589
      end
590
      list.push 'id '    + @id         if @id
591
      list.push 'for <'  + @_for + '>' if @_for
592

    
593
      first = true
594
      list.each do |i|
595
        strategy.space unless first
596
        strategy.meta i
597
        first = false
598
      end
599
      if @date
600
        strategy.meta ';'
601
        strategy.space
602
        strategy.meta time2str(@date)
603
      end
604
    end
605

    
606
  end
607

    
608

    
609
  class KeywordsHeader < StructuredHeader
610

    
611
    PARSE_TYPE = :KEYWORDS
612

    
613
    def keys
614
      ensure_parsed
615
      @keys
616
    end
617

    
618
    private
619

    
620
    def init
621
      @keys = []
622
    end
623

    
624
    def set( a )
625
      @keys = a
626
    end
627

    
628
    def isempty?
629
      @keys.empty?
630
    end
631

    
632
    def do_accept( strategy )
633
      first = true
634
      @keys.each do |i|
635
        if first
636
          first = false
637
        else
638
          strategy.meta ','
639
        end
640
        strategy.meta i
641
      end
642
    end
643

    
644
  end
645

    
646

    
647
  class EncryptedHeader < StructuredHeader
648

    
649
    PARSE_TYPE = :ENCRYPTED
650

    
651
    def encrypter
652
      ensure_parsed
653
      @encrypter
654
    end
655

    
656
    def encrypter=( arg )
657
      ensure_parsed
658
      @encrypter = arg
659
    end
660

    
661
    def keyword
662
      ensure_parsed
663
      @keyword
664
    end
665

    
666
    def keyword=( arg )
667
      ensure_parsed
668
      @keyword = arg
669
    end
670

    
671
    private
672

    
673
    def init
674
      @encrypter = nil
675
      @keyword = nil
676
    end
677

    
678
    def set( args )
679
      @encrypter, @keyword = args
680
    end
681

    
682
    def isempty?
683
      not (@encrypter or @keyword)
684
    end
685

    
686
    def do_accept( strategy )
687
      if @key
688
        strategy.meta @encrypter + ','
689
        strategy.space
690
        strategy.meta @keyword
691
      else
692
        strategy.meta @encrypter
693
      end
694
    end
695

    
696
  end
697

    
698

    
699
  class MimeVersionHeader < StructuredHeader
700

    
701
    PARSE_TYPE = :MIMEVERSION
702

    
703
    def major
704
      ensure_parsed
705
      @major
706
    end
707

    
708
    def major=( arg )
709
      ensure_parsed
710
      @major = arg
711
    end
712

    
713
    def minor
714
      ensure_parsed
715
      @minor
716
    end
717

    
718
    def minor=( arg )
719
      ensure_parsed
720
      @minor = arg
721
    end
722

    
723
    def version
724
      sprintf('%d.%d', major, minor)
725
    end
726

    
727
    private
728

    
729
    def init
730
      @major = nil
731
      @minor = nil
732
    end
733

    
734
    def set( args )
735
      @major, @minor = *args
736
    end
737

    
738
    def isempty?
739
      not (@major or @minor)
740
    end
741

    
742
    def do_accept( strategy )
743
      strategy.meta sprintf('%d.%d', @major, @minor)
744
    end
745

    
746
  end
747

    
748

    
749
  class ContentTypeHeader < StructuredHeader
750

    
751
    PARSE_TYPE = :CTYPE
752

    
753
    def main_type
754
      ensure_parsed
755
      @main
756
    end
757

    
758
    def main_type=( arg )
759
      ensure_parsed
760
      @main = arg.downcase
761
    end
762

    
763
    def sub_type
764
      ensure_parsed
765
      @sub
766
    end
767

    
768
    def sub_type=( arg )
769
      ensure_parsed
770
      @sub = arg.downcase
771
    end
772

    
773
    def content_type
774
      ensure_parsed
775
      @sub ? sprintf('%s/%s', @main, @sub) : @main
776
    end
777

    
778
    def params
779
      ensure_parsed
780
      unless @params.blank?
781
        @params.each do |k, v|
782
          @params[k] = unquote(v)
783
        end
784
      end
785
      @params
786
    end
787

    
788
    def []( key )
789
      ensure_parsed
790
      @params and unquote(@params[key])
791
    end
792

    
793
    def []=( key, val )
794
      ensure_parsed
795
      (@params ||= {})[key] = val
796
    end
797

    
798
    private
799

    
800
    def init
801
      @main = @sub = @params = nil
802
    end
803

    
804
    def set( args )
805
      @main, @sub, @params = *args
806
    end
807

    
808
    def isempty?
809
      not (@main or @sub)
810
    end
811

    
812
    def do_accept( strategy )
813
      if @sub
814
        strategy.meta sprintf('%s/%s', @main, @sub)
815
      else
816
        strategy.meta @main
817
      end
818
      @params.each do |k,v|
819
        if v
820
          strategy.meta ';'
821
          strategy.space
822
          strategy.kv_pair k, unquote(v)
823
        end
824
      end
825
    end
826

    
827
  end
828

    
829

    
830
  class ContentTransferEncodingHeader < StructuredHeader
831

    
832
    PARSE_TYPE = :CENCODING
833

    
834
    def encoding
835
      ensure_parsed
836
      @encoding
837
    end
838

    
839
    def encoding=( arg )
840
      ensure_parsed
841
      @encoding = arg
842
    end
843

    
844
    private
845

    
846
    def init
847
      @encoding = nil
848
    end
849

    
850
    def set( s )
851
      @encoding = s
852
    end
853

    
854
    def isempty?
855
      not @encoding
856
    end
857

    
858
    def do_accept( strategy )
859
      strategy.meta @encoding.capitalize
860
    end
861

    
862
  end
863

    
864

    
865
  class ContentDispositionHeader < StructuredHeader
866

    
867
    PARSE_TYPE = :CDISPOSITION
868

    
869
    def disposition
870
      ensure_parsed
871
      @disposition
872
    end
873

    
874
    def disposition=( str )
875
      ensure_parsed
876
      @disposition = str.downcase
877
    end
878

    
879
    def params
880
      ensure_parsed
881
      unless @params.blank?
882
        @params.each do |k, v|
883
          @params[k] = unquote(v)
884
        end
885
      end
886
      @params
887
    end
888

    
889
    def []( key )
890
      ensure_parsed
891
      @params and unquote(@params[key])
892
    end
893

    
894
    def []=( key, val )
895
      ensure_parsed
896
      (@params ||= {})[key] = val
897
    end
898

    
899
    private
900

    
901
    def init
902
      @disposition = @params = nil
903
    end
904

    
905
    def set( args )
906
      @disposition, @params = *args
907
    end
908

    
909
    def isempty?
910
      not @disposition and (not @params or @params.empty?)
911
    end
912

    
913
    def do_accept( strategy )
914
      strategy.meta @disposition
915
      @params.each do |k,v|
916
        strategy.meta ';'
917
        strategy.space
918
        strategy.kv_pair k, unquote(v)
919
      end
920
    end
921
      
922
  end
923

    
924

    
925
  class HeaderField   # redefine
926

    
927
    FNAME_TO_CLASS = {
928
      'date'                      => DateTimeHeader,
929
      'resent-date'               => DateTimeHeader,
930
      'to'                        => AddressHeader,
931
      'cc'                        => AddressHeader,
932
      'bcc'                       => AddressHeader,
933
      'from'                      => AddressHeader,
934
      'reply-to'                  => AddressHeader,
935
      'resent-to'                 => AddressHeader,
936
      'resent-cc'                 => AddressHeader,
937
      'resent-bcc'                => AddressHeader,
938
      'resent-from'               => AddressHeader,
939
      'resent-reply-to'           => AddressHeader,
940
      'sender'                    => SingleAddressHeader,
941
      'resent-sender'             => SingleAddressHeader,
942
      'return-path'               => ReturnPathHeader,
943
      'message-id'                => MessageIdHeader,
944
      'resent-message-id'         => MessageIdHeader,
945
      'in-reply-to'               => ReferencesHeader,
946
      'received'                  => ReceivedHeader,
947
      'references'                => ReferencesHeader,
948
      'keywords'                  => KeywordsHeader,
949
      'encrypted'                 => EncryptedHeader,
950
      'mime-version'              => MimeVersionHeader,
951
      'content-type'              => ContentTypeHeader,
952
      'content-transfer-encoding' => ContentTransferEncodingHeader,
953
      'content-disposition'       => ContentDispositionHeader,
954
      'content-id'                => MessageIdHeader,
955
      'subject'                   => UnstructuredHeader,
956
      'comments'                  => UnstructuredHeader,
957
      'content-description'       => UnstructuredHeader
958
    }
959

    
960
  end
961

    
962
end   # module TMail
(2-2/4)