lib/mail/field.rb



# encoding: utf-8
module Mail
  # Provides a single class to call to create a new structured or unstructured
  # field.  Works out per RFC what field of field it is being given and returns
  # the correct field of class back on new.
  # 
  # ===Per RFC 2822
  #  
  #  2.2. Header Fields
  #  
  #     Header fields are lines composed of a field name, followed by a colon
  #     (":"), followed by a field body, and terminated by CRLF.  A field
  #     name MUST be composed of printable US-ASCII characters (i.e.,
  #     characters that have values between 33 and 126, inclusive), except
  #     colon.  A field body may be composed of any US-ASCII characters,
  #     except for CR and LF.  However, a field body may contain CRLF when
  #     used in header "folding" and  "unfolding" as described in section
  #     2.2.3.  All field bodies MUST conform to the syntax described in
  #     sections 3 and 4 of this standard.
  #  
  class Field
    
    include Patterns
    include Comparable
    
    STRUCTURED_FIELDS = %w[ bcc cc content-description content-disposition
                            content-id content-location content-transfer-encoding
                            content-type date from in-reply-to keywords message-id
                            mime-version received references reply-to
                            resent-bcc resent-cc resent-date resent-from
                            resent-message-id resent-sender resent-to
                            return-path sender to ]

    KNOWN_FIELDS = STRUCTURED_FIELDS + ['comments', 'subject']
    
    # Generic Field Exception
    class FieldError < StandardError
    end

    # Raised when a parsing error has occurred (ie, a StructuredField has tried
    # to parse a field that is invalid or improperly written)
    class ParseError < FieldError #:nodoc:
    end

    # Raised when attempting to set a structured field's contents to an invalid syntax
    class SyntaxError < FieldError #:nodoc:
    end
    
    # Accepts a string:
    # 
    #  Field.new("field-name: field data")
    # 
    # Or name, value pair:
    # 
    #  Field.new("field-name", "value")
    # 
    # Or a name by itself:
    # 
    #  Field.new("field-name")
    # 
    # Note, does not want a terminating carriage return.  Returns
    # self appropriately parsed.  If value is not a string, then
    # it will be passed through as is, for example, content-type
    # field can accept an array with the type and a hash of 
    # parameters:
    # 
    #  Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
    def initialize(name, value = nil)
      case
      when name =~ /:/ && value.blank?   # Field.new("field-name: field data")
        name, value = split(name)
        create_field(name, value)
      when name !~ /:/ && value.blank?  # Field.new("field-name")
        create_field(name, nil)
      else                              # Field.new("field-name", "value")
        create_field(name, value)
      end
      return self
    end

    def field=(value)
      @field = value
    end
    
    def field
      @field
    end
    
    def name
      field.name
    end
    
    def value
      field.value
    end
    
    def value=(str)
      create_field(name, str)
    end
    
    def to_s
      field.to_s
    end
    
    def update(name, value)
      create_field(name, value)
    end
    
    def same( other )
      match_to_s(other.name, field.name)
    end
    
    def <=>( other )
      self_order = FIELD_ORDER.rindex(self.name.to_s.downcase) || 100
      other_order = FIELD_ORDER.rindex(other.name.to_s.downcase) || 100
      self_order <=> other_order
    end
    
    def method_missing(name, *args, &block)
      field.send(name, *args, &block)
    end
    
    FIELD_ORDER = %w[ return-path received
                      resent-date resent-from resent-sender resent-to
                      resent-cc resent-bcc resent-message-id
                      date from sender reply-to to cc bcc
                      message-id in-reply-to references
                      subject comments keywords
                      mime-version content-type content-transfer-encoding
                      content-location content-disposition content-description ]
    
    private
    
    def split(raw_field)
      match_data = raw_field.match(/^(#{FIELD_NAME})\s*:\s*(#{FIELD_BODY})?$/)
      [match_data[1].to_s.strip, match_data[2].to_s.strip]
    rescue
      STDERR.puts "WARNING: Could not parse (and so ignorning) '#{raw_field}'"
    end

    def create_field(name, value)
      begin
        self.field = new_field(name, value)
      rescue
        self.field = Mail::UnstructuredField.new(name, value)
      end
    end

    def new_field(name, value)
      # Could do this with constantize and make it "as DRY as", but a simple case 
      # statement is, well, simpler... 
      case name.to_s
      when /^to$/i
        ToField.new(name, value)
      when /^cc$/i
        CcField.new(name, value)
      when /^bcc$/i
        BccField.new(name, value)
      when /^message-id$/i
        MessageIdField.new(name, value)
      when /^in-reply-to$/i
        InReplyToField.new(name, value)
      when /^references$/i
        ReferencesField.new(name, value)
      when /^subject$/i
        SubjectField.new(name, value)
      when /^comments$/i
        CommentsField.new(name, value)
      when /^keywords$/i
        KeywordsField.new(name, value)
      when /^date$/i
        DateField.new(name, value)
      when /^from$/i
        FromField.new(name, value)
      when /^sender$/i
        SenderField.new(name, value)
      when /^reply-to$/i
        ReplyToField.new(name, value)
      when /^resent-date$/i
        ResentDateField.new(name, value)
      when /^resent-from$/i
        ResentFromField.new(name, value)
      when /^resent-sender$/i 
        ResentSenderField.new(name, value)
      when /^resent-to$/i
        ResentToField.new(name, value)
      when /^resent-cc$/i
        ResentCcField.new(name, value)
      when /^resent-bcc$/i
        ResentBccField.new(name, value)
      when /^resent-message-id$/i
        ResentMessageIdField.new(name, value)
      when /^return-path$/i
        ReturnPathField.new(name, value)
      when /^received$/i
        ReceivedField.new(name, value)
      when /^mime-version$/i
        MimeVersionField.new(name, value)
      when /^content-transfer-encoding$/i
        ContentTransferEncodingField.new(name, value)
      when /^content-description$/i
        ContentDescriptionField.new(name, value)
      when /^content-disposition$/i
        ContentDispositionField.new(name, value)
      when /^content-type$/i
        ContentTypeField.new(name, value)
      when /^content-id$/i
        ContentIdField.new(name, value)
      when /^content-location$/i
        ContentLocationField.new(name, value)
      else 
        OptionalField.new(name, value)
      end
      
    end

  end
  
end