# encoding: utf-8 module Mail # Provides access to a header object. # # ===Per RFC2822 # # 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 Header include Patterns include Utilities include Enumerable # Creates a new header object. # # Accepts raw text or nothing. If given raw text will attempt to parse # it and split it into the various fields, instantiating each field as # it goes. # # If it finds a field that should be a structured field (such as content # type), but it fails to parse it, it will simply make it an unstructured # field and leave it alone. This will mean that the data is preserved but # no automatic processing of that field will happen. If you find one of # these cases, please make a patch and send it in, or at the least, send # me the example so we can fix it. def initialize(header_text = nil, charset = nil) @errors = [] @charset = charset self.raw_source = header_text.to_crlf split_header if header_text end # The preserved raw source of the header as you passed it in, untouched # for your Regexing glory. def raw_source @raw_source end # Returns an array of all the fields in the header in order that they # were read in. def fields @fields ||= FieldList.new end # 3.6. Field definitions # # It is important to note that the header fields are not guaranteed to # be in a particular order. They may appear in any order, and they # have been known to be reordered occasionally when transported over # the Internet. However, for the purposes of this standard, header # fields SHOULD NOT be reordered when a message is transported or # transformed. More importantly, the trace header fields and resent # header fields MUST NOT be reordered, and SHOULD be kept in blocks # prepended to the message. See sections 3.6.6 and 3.6.7 for more # information. # # Populates the fields container with Field objects in the order it # receives them in. # # Acceps an array of field string values, for example: # # h = Header.new # h.fields = ['From: mikel@me.com', 'To: bob@you.com'] def fields=(unfolded_fields) @fields = Mail::FieldList.new warn "Warning: more than 1000 header fields only using the first 1000" if unfolded_fields.length > 1000 unfolded_fields[0..1000].each do |field| field = Field.new(field, nil, charset) field.errors.each { |error| self.errors << error } selected = select_field_for(field.name) if selected.any? && limited_field?(field.name) selected.first.update(field.name, field.value) else @fields << field end end end def errors @errors end # 3.6. Field definitions # # The following table indicates limits on the number of times each # field may occur in a message header as well as any special # limitations on the use of those fields. An asterisk next to a value # in the minimum or maximum column indicates that a special restriction # appears in the Notes column. # # # # As per RFC, many fields can appear more than once, we will return a string # of the value if there is only one header, or if there is more than one # matching header, will return an array of values in order that they appear # in the header ordered from top to bottom. # # Example: # # h = Header.new # h.fields = ['To: mikel@me.com', 'X-Mail-SPAM: 15', 'X-Mail-SPAM: 20'] # h['To'] #=> 'mikel@me.com' # h['X-Mail-SPAM'] #=> ['15', '20'] def [](name) name = dasherize(name).downcase selected = select_field_for(name) case when selected.length > 1 selected.map { |f| f } when !selected.blank? selected.first else nil end end # Sets the FIRST matching field in the header to passed value, or deletes # the FIRST field matched from the header if passed nil # # Example: # # h = Header.new # h.fields = ['To: mikel@me.com', 'X-Mail-SPAM: 15', 'X-Mail-SPAM: 20'] # h['To'] = 'bob@you.com' # h['To'] #=> 'bob@you.com' # h['X-Mail-SPAM'] = '10000' # h['X-Mail-SPAM'] # => ['15', '20', '10000'] # h['X-Mail-SPAM'] = nil # h['X-Mail-SPAM'] # => nil def []=(name, value) name = dasherize(name) fn = name.downcase selected = select_field_for(fn) case # User wants to delete the field when !selected.blank? && value == nil fields.delete_if { |f| selected.include?(f) } # User wants to change the field when !selected.blank? && limited_field?(fn) selected.first.update(fn, value) # User wants to create the field else # Need to insert in correct order for trace fields self.fields << Field.new(name.to_s, value, charset) end end def charset params = self[:content_type].parameters rescue nil if params params[:charset] else @charset end end def charset=(val) params = self[:content_type].parameters rescue nil if params params[:charset] = val end @charset = val end LIMITED_FIELDS = %w[ date from sender reply-to to cc bcc message-id in-reply-to references subject return-path content-type mime-version content-transfer-encoding content-description content-id content-disposition content-location] def encoded buffer = '' fields.each do |field| buffer << field.encoded end buffer end def to_s encoded end def decoded raise NoMethodError, 'Can not decode an entire header as there could be character set conflicts, try calling #decoded on the various fields.' end def field_summary fields.map { |f| "<#{f.name}: #{f.value}>" }.join(", ") end # Returns true if the header has a Message-ID defined (empty or not) def has_message_id? !fields.select { |f| f.responsible_for?('Message-ID') }.empty? end # Returns true if the header has a Content-ID defined (empty or not) def has_content_id? !fields.select { |f| f.responsible_for?('Content-ID') }.empty? end # Returns true if the header has a Date defined (empty or not) def has_date? !fields.select { |f| f.responsible_for?('Date') }.empty? end # Returns true if the header has a MIME version defined (empty or not) def has_mime_version? !fields.select { |f| f.responsible_for?('Mime-Version') }.empty? end private def raw_source=(val) @raw_source = val end # 2.2.3. Long Header Fields # # The process of moving from this folded multiple-line representation # of a header field to its single line representation is called # "unfolding". Unfolding is accomplished by simply removing any CRLF # that is immediately followed by WSP. Each header field should be # treated in its unfolded form for further syntactic and semantic # evaluation. def unfold(string) string.gsub(/#{CRLF}#{WSP}+/, ' ').gsub(/#{WSP}+/, ' ') end # Returns the header with all the folds removed def unfolded_header @unfolded_header ||= unfold(raw_source) end # Splits an unfolded and line break cleaned header into individual field # strings. def split_header self.fields = unfolded_header.split(CRLF) end def select_field_for(name) fields.select { |f| f.responsible_for?(name.to_s) } end def limited_field?(name) LIMITED_FIELDS.include?(name.to_s.downcase) end end end