123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- # 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.
- #
- # <snip table from 3.6>
- #
- # 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
|