# encoding: utf-8 # frozen_string_literal: true require 'mail/constants' require 'mail/utilities' 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 Enumerable @@maximum_amount = 1000 # Large amount of headers in Email might create extra high CPU load # Use this parameter to limit number of headers that will be parsed by # mail library. # Default: 1000 def self.maximum_amount @@maximum_amount end def self.maximum_amount=(value) @@maximum_amount = value end attr_reader :raw_source, :charset # 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) @charset = charset @raw_source = ::Mail::Utilities.to_crlf(header_text).lstrip split_header if header_text end def initialize_copy(original) super @fields = @fields.dup @fields.map!(&:dup) 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 if unfolded_fields.size > self.class.maximum_amount Kernel.warn "WARNING: More than #{self.class.maximum_amount} header fields; only using the first #{self.class.maximum_amount} and ignoring the rest" unfolded_fields = unfolded_fields.slice(0...self.class.maximum_amount) end unfolded_fields.each do |field| if field = Field.parse(field, charset) @fields.add_field field end end end def errors @fields.map(&:errors).flatten(1) 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) fields.get_field(Utilities.dasherize(name)) 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 = name.to_s if name.include?(Constants::COLON) raise ArgumentError, "Header names may not contain a colon: #{name.inspect}" end name = Utilities.dasherize(name) # Assign nil to delete the field if value.nil? fields.delete_field name else fields.add_field Field.new(name.to_s, value, charset) # Update charset if specified in Content-Type if name == 'content-type' params = self[:content_type].parameters rescue nil @charset = params[:charset] if params && params[:charset] end end end def charset=(val) params = self[:content_type].parameters rescue nil if params if val params[:charset] = val else params.delete(:charset) end end @charset = val end def encoded buffer = String.new buffer.force_encoding('us-ascii') if buffer.respond_to?(:force_encoding) 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.summary end # Returns true if the header has a Message-ID defined (empty or not) def has_message_id? fields.has_field? 'Message-ID' end # Returns true if the header has a Content-ID defined (empty or not) def has_content_id? fields.has_field? 'Content-ID' end # Returns true if the header has a Date defined (empty or not) def has_date? fields.has_field? 'Date' end # Returns true if the header has a MIME version defined (empty or not) def has_mime_version? fields.has_field? 'Mime-Version' end private # Splits an unfolded and line break cleaned header into individual field # strings. def split_header self.fields = @raw_source.split(Constants::HEADER_SPLIT) end # Enumerable support. Yield each field in order. def each(&block) fields.each(&block) end end end