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

require 'tmail/encode'
require 'tmail/address'
require 'tmail/parser'
require 'tmail/config'
require 'tmail/utils'

#:startdoc:
module TMail

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

    include TextUtils

    class << self

      alias newobj new

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

      # Returns a HeaderField object matching the header you specify in the "name" param.
      # Requires an initialized TMail::Port to be passed in.
      #
      # The method searches the header of the Port you pass into it to find a match on
      # the header line you pass.  Once a match is found, it will unwrap the matching line
      # as needed to return an initialized HeaderField object.
      #
      # If you want to get the Envelope sender of the email object, pass in "EnvelopeSender",
      # if you want the From address of the email itself, pass in 'From'.
      #
      # This is because a mailbox doesn't have the : after the From that designates the
      # beginning of the envelope sender (which can be different to the from address of 
      # the emial)
      #
      # Other fields can be passed as normal, "Reply-To", "Received" etc.
      #
      # Note: Change of behaviour in 1.2.1 => returns nil if it does not find the specified
      # header field, otherwise returns an instantiated object of the correct header class
      # 
      # For example:
      #   port = TMail::FilePort.new("/test/fixtures/raw_email_simple")
      #   h = TMail::HeaderField.new_from_port(port, "From")
      #   h.addrs.to_s #=> "Mikel Lindsaar <mikel@nowhere.com>"
      #   h = TMail::HeaderField.new_from_port(port, "EvelopeSender")
      #   h.addrs.to_s #=> "mike@anotherplace.com.au"
      #   h = TMail::HeaderField.new_from_port(port, "SomeWeirdHeaderField")
      #   h #=> nil
      def new_from_port( port, name, conf = DEFAULT_CONFIG )
        if name == "EnvelopeSender"
          name = "From"
          re = Regexp.new('\A(From) ', 'i')
        else
          re = Regexp.new('\A(' + Regexp.quote(name) + '):', 'i')
        end
        str = nil
        port.ropen {|f|
            f.each do |line|
              if m = re.match(line)            then str = m.post_match.strip
              elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
              elsif /\A-*\s*\z/ === line       then break
              elsif str                        then break
              end
            end
        }
        new(name, str, Config.to_config(conf)) if str
      end

      def internal_new( name, conf )
        FNAME_TO_CLASS[name].newobj('', conf, true)
      end

    end   # class << self

    def initialize( body, conf, intern = false )
      @body = body
      @config = conf

      @illegal = false
      @parsed = false
      
      if intern
        @parsed = true
        parse_init
      end
    end

    def inspect
      "#<#{self.class} #{@body.inspect}>"
    end

    def illegal?
      @illegal
    end

    def empty?
      ensure_parsed
      return true if @illegal
      isempty?
    end

    private

    def ensure_parsed
      return if @parsed
      @parsed = true
      parse
    end

    # defabstract parse
    # end

    def clear_parse_status
      @parsed = false
      @illegal = false
    end

    public

    def body
      ensure_parsed
      v = Decoder.new(s = '')
      do_accept v
      v.terminate
      s
    end

    def body=( str )
      @body = str
      clear_parse_status
    end

    include StrategyInterface

    def accept( strategy )
      ensure_parsed
      do_accept strategy
      strategy.terminate
    end

    # abstract do_accept

  end


  class UnstructuredHeader < HeaderField

    def body
      ensure_parsed
      @body
    end

    def body=( arg )
      ensure_parsed
      @body = arg
    end

    private

    def parse_init
    end

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

    def isempty?
      not @body
    end

    def do_accept( strategy )
      strategy.text @body
    end

  end


  class StructuredHeader < HeaderField

    def comments
      ensure_parsed
      if @comments[0]
        [Decoder.decode(@comments[0])]
      else
        @comments
      end
    end

    private

    def parse
      save = nil

      begin
        parse_init
        do_parse
      rescue SyntaxError
        if not save and mime_encoded? @body
          save = @body
          @body = Decoder.decode(save)
          retry
        elsif save
          @body = save
        end

        @illegal = true
        raise if @config.strict_parse?
      end
    end

    def parse_init
      @comments = []
      init
    end

    def do_parse
      quote_boundary
      obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
      set obj if obj
    end

  end


  class DateTimeHeader < StructuredHeader

    PARSE_TYPE = :DATETIME

    def date
      ensure_parsed
      @date
    end

    def date=( arg )
      ensure_parsed
      @date = arg
    end

    private

    def init
      @date = nil
    end

    def set( t )
      @date = t
    end

    def isempty?
      not @date
    end

    def do_accept( strategy )
      strategy.meta time2str(@date)
    end

  end


  class AddressHeader < StructuredHeader

    PARSE_TYPE = :MADDRESS

    def addrs
      ensure_parsed
      @addrs
    end

    private

    def init
      @addrs = []
    end

    def set( a )
      @addrs = a
    end

    def isempty?
      @addrs.empty?
    end

    def do_accept( strategy )
      first = true
      @addrs.each do |a|
        if first
          first = false
        else
          strategy.meta ','
          strategy.space
        end
        a.accept strategy
      end

      @comments.each do |c|
        strategy.space
        strategy.meta '('
        strategy.text c
        strategy.meta ')'
      end
    end

  end


  class ReturnPathHeader < AddressHeader

    PARSE_TYPE = :RETPATH

    def addr
      addrs()[0]
    end

    def spec
      a = addr() or return nil
      a.spec
    end

    def routes
      a = addr() or return nil
      a.routes
    end

    private

    def do_accept( strategy )
      a = addr()

      strategy.meta '<'
      unless a.routes.empty?
        strategy.meta a.routes.map {|i| '@' + i }.join(',')
        strategy.meta ':'
      end
      spec = a.spec
      strategy.meta spec if spec
      strategy.meta '>'
    end

  end


  class SingleAddressHeader < AddressHeader

    def addr
      addrs()[0]
    end

    private

    def do_accept( strategy )
      a = addr()
      a.accept strategy
      @comments.each do |c|
        strategy.space
        strategy.meta '('
        strategy.text c
        strategy.meta ')'
      end
    end

  end


  class MessageIdHeader < StructuredHeader

    def id
      ensure_parsed
      @id
    end

    def id=( arg )
      ensure_parsed
      @id = arg
    end

    private

    def init
      @id = nil
    end

    def isempty?
      not @id
    end

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

    def do_accept( strategy )
      strategy.meta @id
    end

  end


  class ReferencesHeader < StructuredHeader

    def refs
      ensure_parsed
      @refs
    end

    def each_id
      self.refs.each do |i|
        yield i if MESSAGE_ID === i
      end
    end

    def ids
      ensure_parsed
      @ids
    end

    def each_phrase
      self.refs.each do |i|
        yield i unless MESSAGE_ID === i
      end
    end

    def phrases
      ret = []
      each_phrase {|i| ret.push i }
      ret
    end

    private

    def init
      @refs = []
      @ids = []
    end

    def isempty?
      @ids.empty?
    end

    def do_parse
      str = @body
      while m = MESSAGE_ID.match(str)
        pre = m.pre_match.strip
        @refs.push pre unless pre.empty?
        @refs.push s = m[0]
        @ids.push s
        str = m.post_match
      end
      str = str.strip
      @refs.push str unless str.empty?
    end

    def do_accept( strategy )
      first = true
      @ids.each do |i|
        if first
          first = false
        else
          strategy.space
        end
        strategy.meta i
      end
    end

  end


  class ReceivedHeader < StructuredHeader

    PARSE_TYPE = :RECEIVED

    def from
      ensure_parsed
      @from
    end

    def from=( arg )
      ensure_parsed
      @from = arg
    end

    def by
      ensure_parsed
      @by
    end

    def by=( arg )
      ensure_parsed
      @by = arg
    end

    def via
      ensure_parsed
      @via
    end

    def via=( arg )
      ensure_parsed
      @via = arg
    end

    def with
      ensure_parsed
      @with
    end

    def id
      ensure_parsed
      @id
    end

    def id=( arg )
      ensure_parsed
      @id = arg
    end

    def _for
      ensure_parsed
      @_for
    end

    def _for=( arg )
      ensure_parsed
      @_for = arg
    end

    def date
      ensure_parsed
      @date
    end

    def date=( arg )
      ensure_parsed
      @date = arg
    end

    private

    def init
      @from = @by = @via = @with = @id = @_for = nil
      @with = []
      @date = nil
    end

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

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

    def do_accept( strategy )
      list = []
      list.push 'from '  + @from       if @from
      list.push 'by '    + @by         if @by
      list.push 'via '   + @via        if @via
      @with.each do |i|
        list.push 'with ' + i
      end
      list.push 'id '    + @id         if @id
      list.push 'for <'  + @_for + '>' if @_for

      first = true
      list.each do |i|
        strategy.space unless first
        strategy.meta i
        first = false
      end
      if @date
        strategy.meta ';'
        strategy.space
        strategy.meta time2str(@date)
      end
    end

  end


  class KeywordsHeader < StructuredHeader

    PARSE_TYPE = :KEYWORDS

    def keys
      ensure_parsed
      @keys
    end

    private

    def init
      @keys = []
    end

    def set( a )
      @keys = a
    end

    def isempty?
      @keys.empty?
    end

    def do_accept( strategy )
      first = true
      @keys.each do |i|
        if first
          first = false
        else
          strategy.meta ','
        end
        strategy.meta i
      end
    end

  end


  class EncryptedHeader < StructuredHeader

    PARSE_TYPE = :ENCRYPTED

    def encrypter
      ensure_parsed
      @encrypter
    end

    def encrypter=( arg )
      ensure_parsed
      @encrypter = arg
    end

    def keyword
      ensure_parsed
      @keyword
    end

    def keyword=( arg )
      ensure_parsed
      @keyword = arg
    end

    private

    def init
      @encrypter = nil
      @keyword = nil
    end

    def set( args )
      @encrypter, @keyword = args
    end

    def isempty?
      not (@encrypter or @keyword)
    end

    def do_accept( strategy )
      if @key
        strategy.meta @encrypter + ','
        strategy.space
        strategy.meta @keyword
      else
        strategy.meta @encrypter
      end
    end

  end


  class MimeVersionHeader < StructuredHeader

    PARSE_TYPE = :MIMEVERSION

    def major
      ensure_parsed
      @major
    end

    def major=( arg )
      ensure_parsed
      @major = arg
    end

    def minor
      ensure_parsed
      @minor
    end

    def minor=( arg )
      ensure_parsed
      @minor = arg
    end

    def version
      sprintf('%d.%d', major, minor)
    end

    private

    def init
      @major = nil
      @minor = nil
    end

    def set( args )
      @major, @minor = *args
    end

    def isempty?
      not (@major or @minor)
    end

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

  end


  class ContentTypeHeader < StructuredHeader

    PARSE_TYPE = :CTYPE

    def main_type
      ensure_parsed
      @main
    end

    def main_type=( arg )
      ensure_parsed
      @main = arg.downcase
    end

    def sub_type
      ensure_parsed
      @sub
    end

    def sub_type=( arg )
      ensure_parsed
      @sub = arg.downcase
    end

    def content_type
      ensure_parsed
      @sub ? sprintf('%s/%s', @main, @sub) : @main
    end

    def params
      ensure_parsed
      unless @params.blank?
        @params.each do |k, v|
          @params[k] = unquote(v)
        end
      end
      @params
    end

    def []( key )
      ensure_parsed
      @params and unquote(@params[key])
    end

    def []=( key, val )
      ensure_parsed
      (@params ||= {})[key] = val
    end

    private

    def init
      @main = @sub = @params = nil
    end

    def set( args )
      @main, @sub, @params = *args
    end

    def isempty?
      not (@main or @sub)
    end

    def do_accept( strategy )
      if @sub
        strategy.meta sprintf('%s/%s', @main, @sub)
      else
        strategy.meta @main
      end
      @params.each do |k,v|
        if v
          strategy.meta ';'
          strategy.space
          strategy.kv_pair k, v
        end
      end
    end

  end


  class ContentTransferEncodingHeader < StructuredHeader

    PARSE_TYPE = :CENCODING

    def encoding
      ensure_parsed
      @encoding
    end

    def encoding=( arg )
      ensure_parsed
      @encoding = arg
    end

    private

    def init
      @encoding = nil
    end

    def set( s )
      @encoding = s
    end

    def isempty?
      not @encoding
    end

    def do_accept( strategy )
      strategy.meta @encoding.capitalize
    end

  end


  class ContentDispositionHeader < StructuredHeader

    PARSE_TYPE = :CDISPOSITION

    def disposition
      ensure_parsed
      @disposition
    end

    def disposition=( str )
      ensure_parsed
      @disposition = str.downcase
    end

    def params
      ensure_parsed
      unless @params.blank?
        @params.each do |k, v|
          @params[k] = unquote(v)
        end
      end
      @params
    end

    def []( key )
      ensure_parsed
      @params and unquote(@params[key])
    end

    def []=( key, val )
      ensure_parsed
      (@params ||= {})[key] = val
    end

    private

    def init
      @disposition = @params = nil
    end

    def set( args )
      @disposition, @params = *args
    end

    def isempty?
      not @disposition and (not @params or @params.empty?)
    end

    def do_accept( strategy )
      strategy.meta @disposition
      @params.each do |k,v|
        strategy.meta ';'
        strategy.space
        strategy.kv_pair k, unquote(v)
      end
    end
      
  end


  class HeaderField   # redefine

    FNAME_TO_CLASS = {
      'date'                      => DateTimeHeader,
      'resent-date'               => DateTimeHeader,
      'to'                        => AddressHeader,
      'cc'                        => AddressHeader,
      'bcc'                       => AddressHeader,
      'from'                      => AddressHeader,
      'reply-to'                  => AddressHeader,
      'resent-to'                 => AddressHeader,
      'resent-cc'                 => AddressHeader,
      'resent-bcc'                => AddressHeader,
      'resent-from'               => AddressHeader,
      'resent-reply-to'           => AddressHeader,
      'sender'                    => SingleAddressHeader,
      'resent-sender'             => SingleAddressHeader,
      'return-path'               => ReturnPathHeader,
      'message-id'                => MessageIdHeader,
      'resent-message-id'         => MessageIdHeader,
      'in-reply-to'               => ReferencesHeader,
      'received'                  => ReceivedHeader,
      'references'                => ReferencesHeader,
      'keywords'                  => KeywordsHeader,
      'encrypted'                 => EncryptedHeader,
      'mime-version'              => MimeVersionHeader,
      'content-type'              => ContentTypeHeader,
      'content-transfer-encoding' => ContentTransferEncodingHeader,
      'content-disposition'       => ContentDispositionHeader,
      'content-id'                => MessageIdHeader,
      'subject'                   => UnstructuredHeader,
      'comments'                  => UnstructuredHeader,
      'content-description'       => UnstructuredHeader
    }

  end

end   # module TMail
