tmail/tmail/field.rb

#
# field.rb
#
#   Copyright (c) 1998-1999 Minero Aoki <aamine@dp.u-netsurf.ne.jp>
#
#   This program is free software.
#   You can distribute/modify this program under the terms of
#   the GNU Lesser General Public License version 2 or later.
#

require 'delegate'

require 'amstd/to_s'

require 'tmail/mailp'
require 'tmail/encode'


module TMail


  MSGID = /<[^\@>]+\@[^>\@]+>/o

  ZONESTR_TABLE = {
    'jst' =>   9 * 60,
    'eet' =>   2 * 60,
    'bst' =>   1 * 60,
    'met' =>   1 * 60,
    'gmt' =>   0,
    'utc' =>   0,
    'ut'  =>   0,
    'nst' => -(3 * 60 + 30),
    'ast' =>  -4 * 60,
    'edt' =>  -4 * 60,
    'est' =>  -5 * 60,
    'cdt' =>  -5 * 60,
    'cst' =>  -6 * 60,
    'mdt' =>  -6 * 60,
    'mst' =>  -7 * 60,
    'pdt' =>  -7 * 60,
    'pst' =>  -8 * 60,
    'a'   =>  -1 * 60,
    'b'   =>  -2 * 60,
    'c'   =>  -3 * 60,
    'd'   =>  -4 * 60,
    'e'   =>  -5 * 60,
    'f'   =>  -6 * 60,
    'g'   =>  -7 * 60,
    'h'   =>  -8 * 60,
    'i'   =>  -9 * 60,
    # j not use
    'k'   => -10 * 60,
    'l'   => -11 * 60,
    'm'   => -12 * 60,
    'n'   =>   1 * 60,
    'o'   =>   2 * 60,
    'p'   =>   3 * 60,
    'q'   =>   4 * 60,
    'r'   =>   5 * 60,
    's'   =>   6 * 60,
    't'   =>   7 * 60,
    'u'   =>   8 * 60,
    'v'   =>   9 * 60,
    'w'   =>  10 * 60,
    'x'   =>  11 * 60,
    'y'   =>  12 * 60,
    'z'   =>   0 * 60
  }

  TSPECIAL = %_()<>@,.;:\\"/[]?=_
  CONTROL  = "\\\x00-\\\x20\\\x7f-\\\xff"
  INSECURE = /[#{Regexp.quote TSPECIAL}#{CONTROL}]/o

  class << self

  def msgid?( str )
    MSGID === str
  end

  def zonestr2i( str )
    if /([\+\-])(\d\d?)(\d\d)/ === str then
      minu = $2.to_i * 60 + $3.to_i
      $1 == '-' ? -minu : minu
    else
      unless tmp = ZONESTR_TABLE[ str.downcase ] then
        raise ParseError, "wrong timezone format '#{str}'"
      end
      tmp * 60
    end
  end

  def time2str( time )
    ret = time.strftime( "%a, #{time.mday} %b %Y %X " )

    # Time#gmtime changes self!!!
    tm = Time.at( time.to_i )
    # [ruby-list:7928]
    offset = tm.to_i - Time.mktime( *tm.gmtime.to_a[0, 6].reverse ).to_i
    ret << '%+.2d%.2d' % (offset / 60).divmod( 60 )

    ret
  end

  def quote( str )
    if INSECURE === str then
      %("#{str}")
    else
      str
    end
  end


  # internal use

  def define_header_to_s( mod, eoptarg, doptarg )
    mod.module_eval %^
      def encoded( eol = "\r\n", charset = 'j',
                   ret = '' #{eoptarg ? ',' + eoptarg : ''} )
        ret = ''
        v = ::TMail::HFencoder.new( ret, eol, charset, limit )
        accept v
        v.write_in
        ret
      end
      alias to_s encoded

      def decoded( eol = "\n", charset = 'e',
                   ret = '' #{doptarg ? ',' + doptarg : ''} )
        ret = ''
        v = ::TMail::HFdecoder.new( ret, charset )
        accept v
        v.write_in
        ret
      end
      alias inspect decoded
    ^
  end

  end   # class << self


class HeaderField

  def initialize( fname, fbody, strict )
    @name = fname
    fbody.strip!
    @body = fbody
    @strict = strict

    @parsed = false
    @parsing = false
    @written = false
  end


  R     = (1 << 0)  # readable
  W     = (1 << 1)  # writable
  NODUP = (1 << 2)  # dup?

  class << self

    alias hf_original_new new

    def new( fname, fbody, strict = false )
      if self == HeaderField then
        tmp = fname.downcase
        klass = if /\Ax-/ === tmp
                then StringH
                else STR2CLASS[tmp] || UnknownH
                end

        klass.hf_original_new( fname, fbody, strict )
      else
        hf_original_new( fname, fbody, strict )
      end
    end


    def parse_on_rw
      @parse_on_rw = true
    end

    def parse_on_create
      @parse_on_rw = false
      module_eval %^
        def initialize( fname, fbody, strict )
          super
          parse
        end
      ^
    end

    def header_attr( name, type, flags, ali = nil )
      name = _name2str( name )
      type = _type2str( type )
      r = (flags & R != 0)
      w = (flags & W != 0)
      c = (flags & NODUP != 0)

      if r then
        parse = if @parse_on_rw then
                  %-
                    unless @parsed or @parsing then
                      parse
                    end
                  -
                else
                  ''
                end
        write = unless w then
                  '@written = true'
                else
                  ''
                end
        retrn = if c then
                  "@#{name}"
                else
                  "@#{name} and @#{name}.dup"
                end
        module_eval sprintf( %-
          def #{name}
            %s
            %s
            %s
          end
        -, parse, write, retrn )
      end

      if w then
        parse = if @parse_on_rw then
                  %-
                    unless @parsed or @parsing then
                      parse
                      @written = true
                    end
                  -
                else
                  %-
                    unless @parsing then
                      @written = true
                    end
                  -
                end
        module_eval sprintf( %-
          def #{name}=( arg )
            %s
            @#{name} = arg
          end
        -, parse )
      end

      if ali then
        ali.each do |a|
          na = _name2str( a )
          module_eval %(alias #{na} #{name})
          module_eval %(alias #{na}= #{name}=) if w
        end
      end
    end
        
  end

  # abstract parse


  def hash
    @name.hash
  end

  def eql?( oth )
    self.type === oth and @name == oth.name
  end

  def ==( oth )   # don't alias
    eql? oth
  end
    

  def name
    @name.dup
  end


  def body
    ret = ''
    v = HFdecoder.new( ret )
    if @written then
      do_accept v
    else
      v.header_body @body
    end
    v.write_in
    ret
  end


  def accept( visitor )
    visitor.header_name @name
    unless @written then
      visitor.header_body @body
    else
      do_accept visitor
    end
  end

  ::TMail.define_header_to_s self, 'limit = 72', nil

  def do_accept( visitor )
    visitor.header_body body
  end

end



# string type ---------------------------------------------

class StringH < HeaderField

  def initialize( fname, fbody, strict )
    super
    @decoded = nil
    @written = true
  end

  def body
    unless @decoded then
      @decoded = HFdecoder.decode( @body )
    end
    @decoded.dup
  end

  def body=( str )
    @body = nil
    @decoded = str
  end

  def eql?( oth )
    super and
    body == oth.body
  end

  def do_accept( visitor )
    visitor.text @body || @decoded
  end

end


# struct type -----------------------------------------

class StructH < HeaderField

  parse_on_rw

  header_attr :comments, Array, R | NODUP


  private

  def init
    @comments = []
  end

  def parse
    @parsing = true
    init
    if @strict then
      Mailp.parse( @body, self, nil )
    else
      begin
        Mailp.parse( @body, self, nil )
      rescue ParseError, ScanError
        ;
      end
    end
    @parsing = false
    @parsed = true
  end

end


# unknown type ----------------------------------------

class UnknownH < StructH

  def initialize( fname, fbody, strict )
    super
    @decoded = nil
    @written = true
  end

  def body
    unless @decoded then
      @decoded = Bencode.decode( @body )
    end
    @decoded.dup
  end

  def body=( str )
    @body = nil
    @decoded = str
  end

  def eql?( oth )
    super and
    body == oth.body
  end

  private

  def parse
  end

  def do_accept( visitor )
    visitor.text @body || @decoded
  end

end


# date type -------------------------------------------

class DateH < StructH

  parse_on_rw

  header_attr :date, Time, R | W | NODUP

  def date=( arg )
    @date = arg.localtime
    @written = true
  end

  def eql?( oth )
    super and
    @date == oth.date
  end

  def do_accept( visitor )
    visitor.meta ::TMail.time2str( @date )
  end

end


# return path type -------------------------------------

class RetpathH < StructH

  parse_on_rw

  header_attr :route, Array,  R | NODUP, [:routes]
  header_attr :addr,  String, R | W

  def eql?( oth )
    super and
    self.addr == oth.addr and
    self.routes == oth.routes
  end


  private

  def init
    super
    @route = []
  end

  def do_accept( visitor )
    visitor.meta '<'
    unless @route.empty? then
      last = @routes[-1]
      @route.each do |i|
        s = '@' + i
        s << ',' unless i.equal? last
        visitor.meta
      end
      visitor.meta ':'
    end
    visitor.meta @addr
    visitor.meta '>'
  end

end


# address classes ---------------------------------------------


class Address

  def initialize( local, domain )
    @local = local
    @domain = domain
    @phrase = nil
    @route = []
  end

  attr :phrase, true

  attr :route
  alias routes route
  attr_writer :route   # internal use only

  def address
    s = @local.collect {|i| ::TMail.quote i }.join('.')
    s << '@' << @domain.collect {|i| ::TMail.quote i }.join('.') if @domain
    s
  end

  def address=( str )
    tmp = str.split( '@', 2 )
    @local  = tmp[0].split('.')
    @domain = tmp[1].split('.')
  end

  alias spec address
  alias spec= address=
  alias addr address
  alias addr= address=


  def eql?( other )
    self.type === other        and
    addr      ==  other.addr   and
    route     ==  other.route  and
    phrase    ==  other.phrase
  end
  alias == eql?

  def dup
    i = nil
    n = type.new( @local, @domain )
    n.phrase = @phrase.dup if @phrase
    n.route = @routes.collect{|i| i.dup } unless @routes.empty?
    n
  end

  def accept( visitor )
    spec_p = !@phrase and @route.empty?

    if @phrase then
      visitor.text @phrase
      visitor.spc
    end
    tmp = spec_p ? '' : '<'
    unless @route.empty? then
      tmp << @route.collect {|i| '@' + i }.join( ',' ) << ':'
    end
    tmp << address
    tmp << '>' unless spec_p
    visitor.meta tmp
  end

  ::TMail.define_header_to_s self, nil, nil

end



class AddressGroup < DelegateClass( Array )

  def initialize( name, arg = nil )
    @name = name
    super arg ? arg.dup : []
  end


  attr :name, true
  
  def eql?( oth )
    super( oth ) and self.name == oth.name
  end
  alias == eql?

  def each_address
    bare_each do |mbox|
      if AddressGroup === mbox then
        mbox.each {|i| yield i }
      else
        yield mbox
      end
    end
  end

  def accept( visitor )
    visitor.text @name
    visitor.meta ':'
    visitor.spc
    last = self[-1]
    bare_each do |mbox|
      mbox.accept visitor
      visitor.meta mbox.equal?(last) ? ';' : ','
    end
  end

  ::TMail.define_header_to_s self, nil, nil

end


# saddr type -------------------------------------------

class SaddrH < StructH
  
  parse_on_rw

  header_attr :addr, Address, R | W

  def eql?( oth )
    super and
    self.addr == oth.addr
  end

  def do_accept( visitor )
    @addr.accept visitor
    visitor.comments @comments unless @comments.empty?
  end

end

class SmboxH < SaddrH
end


# maddr type -----------------------------------------

class MaddrH < StructH

  parse_on_rw

  header_attr :addrs, Array, R | NODUP

  def eql?( oth )
    super and
    addrs == oth.addrs
  end


  private

  def init
    super
    @addrs = []
  end

  def do_accept( visitor )
    first = true
    last = @addrs[-1]
    @addrs.each do |a|
      if first then
        first = false
      else
        visitor.spc
      end
      a.accept visitor
      visitor.meta ',' unless a.equal? last
    end
    unless @comments.empty? then
      visitor.spc
      @comments.each do |c|
        visitor.meta '('
        visitor.text c
        visitor.meta ')'
      end
    end
  end

end

class MmboxH < MaddrH

=begin
  def do_accept( visitor )
    first = true
    last = @addrs[-1]
  end
=end

end


# ref type -------------------------------------------

class RefH < StructH

  parse_on_rw

  header_attr :refs, Array, R | NODUP

  def eql?( oth )
    super and
    self.refs == oth.refs
  end

  def each_msgid
    refs.each do |i|
      yield i if ::TMail.msgid? i
    end
  end

  def each_phrase
    refs.each do |i|
      yield i unless ::TMail.msgid? i
    end
  end


  private

  def init
    super
    @refs = []
  end

  def do_accept( visitor )
    first = true
    @refs.each do |i|
      if first then
        first = false
      else
        visitor.spc
      end
      if ::TMail.msgid? i then
        visitor.meta i
      else
        visitor.text i
      end
    end
  end

end


# key type -------------------------------------------

class KeyH < StructH

  parse_on_rw

  header_attr :keys, Array, R | NODUP

  def eql?( oth )
    super and
    self.keys == oth.keys
  end


  private

  def init
    super
    @keys = []
  end

  def do_accept( visitor )
    save = @keys.pop
    @keys.each do |i|
      visitor.meta i + ','
    end
    visitor.meta save
    @keys.push save
  end

end


# received type ---------------------------------------

class RecvH < StructH

  parse_on_rw

  header_attr :from,  String, R | W
  header_attr :by,    String, R | W
  header_attr :via,   String, R | W
  header_attr :with,  Array,  R | NODUP
  header_attr :msgid, String, R | W
  header_attr :for_,  String, R | W,        [:ford, :for_domain]
  header_attr :date,  Time,   R | W | NODUP


  def eql?( oth )
    super and
    from  == oth.from and
    by    == oth.by and
    via   == oth.via and
    with  == oth.with and
    msgid == oth.msgid and
    for_  == oth.for_ and
    date  == oth.date
  end


  private

  def init
    super
    @with = []
  end

  Tag = %w( from by via with id for )

  def do_accept( visitor )
    val = [ @from, @by, @via ] + @with + [ @msgid, @for_ ]
    val.compact!
    c   = [ @from ? 1 : 0, @by ? 1 : 0, @via ? 1 : 0,
            @with.size, @msgid ? 1 : 0, @for_ ? 1 : 0 ]

    i = 0
    c.each_with_index do |count, idx|
      label = Tag[idx]
      count.times do
        visitor.spc unless i == 0
        visitor.meta label
        visitor.spc
        visitor.meta val[i]
        i += 1
      end
    end
    if @date then
      visitor.meta ';'
      visitor.spc
      visitor.meta ::TMail.time2str( @date )
    end
  end

end


# message-id type  ----------------------------------------------------

class MsgidH < StructH

  parse_on_rw

  def initialize( fname, fbody, strict )
    super
    @msgid = fbody.strip
  end

  header_attr :msgid, String, R | W
  
  def eql?( oth )
    super and
    msgid == oth.msgid
  end

  def do_accept( visitor )
    visitor.meta @msgid
  end

end


# encrypted type ------------------------------------------------------

class EncH < StructH

  parse_on_rw

  header_attr :encrypter, String, R | W
  header_attr :keyword,   String, R | W

  def eql?( oth )
    super and
    self.encrypter == oth.encrypter and
    self.keyword   == oth.keyword
  end

  def init
    super
    @keyword = nil
  end

  def do_accept( visitor )
    if @key then
      visitor.meta @encrypter + ','
      visitor.spc
      visitor.meta @key
    else
      visitor.meta @encrypter
    end
  end

end


# version type -----------------------------------------

class VersionH < StructH

  parse_on_rw

  header_attr :major, :Integer, R | W | NODUP
  header_attr :minor, :Integer, R | W | NODUP

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

  def eql?( oth )
    super and
    self.major == oth.major and
    self.minor == oth.minor
  end

  def do_accept( visitor )
    visitor.meta "#{@major}.#{@minor}"
  end

end


# content type  ---------------------------------------


class CTypeH < StructH

  parse_on_create

  header_attr :main,   :String, R | W
  header_attr :sub,    :String, R | W
  header_attr :params, :Hash,   R | NODUP

  def []( key )
    params[key]
  end

  def eql?( oth )
    super and
    main == oth.main and
    sub  == oth.sub  and
    params == oth.params
  end


  private

  def init
    super
    @params = {}
  end

  def do_accept( visitor )
    visitor.meta "#{@main}/#{@sub}"
    @params.each do |k,v|
      visitor.meta ';'
      visitor.spc
      visitor.meta k
      visitor.meta '='
      visitor.text v
    end
  end

end


# encoding type  ----------------------------

class CEncodingH < StructH

  parse_on_rw

  header_attr :encoding, :String, R | W

  def eql?( oth )
    super and
    encoding == oth.encoding
  end

  def do_accept( visitor )
    visitor.meta @encoding
  end

end


# disposition type ----------------------------------

class CDispositionH < StructH

  parse_on_rw

  header_attr :disposition, :String, R | W
  header_attr :params,      :Hash,   R | NODUP

  def []( key )
    params[key]
  end

  def eql?( oth )
    super and
    self.disposition == oth.disposition and
    self.params == oth.params
  end
  

  private

  def init
    super
    @params = {}
  end

  def do_accept( visitor )
    visitor.meta @disposition
    @params.each do |k,v|
      visitor.meta ';'
      visitor.spc
      visitor.meta k
      visitor.meta '='
      visitor.text v
    end
  end
    
end


# ---------------------------------------------------

class HeaderField   # backward definition

  STR2CLASS = {
    'date'                      => DateH,
    'resent-date'               => DateH,
    'received'                  => RecvH,
    'return-path'               => RetpathH,
    'sender'                    => SaddrH,
    'resent-sender'             => SaddrH,
    'to'                        => MaddrH,
    'cc'                        => MaddrH,
    'bcc'                       => MaddrH,
    'from'                      => MmboxH,
    'reply-to'                  => MaddrH,
    'resent-to'                 => MaddrH,
    'resent-cc'                 => MaddrH,
    'resent-bcc'                => MaddrH,
    'resent-from'               => MmboxH,
    'resent-reply-to'           => MaddrH,
    'message-id'                => MsgidH,
    'resent-message-id'         => MsgidH,
    'content-id'                => MsgidH,
    'in-reply-to'               => RefH,
    'references'                => RefH,
    'keywords'                  => KeyH,
    'encrypted'                 => EncH,
    'mime-version'              => VersionH,
    'content-type'              => CTypeH,
    'content-transfer-encoding' => CEncodingH,
    'content-disposition'       => CDispositionH,
    'subject'                   => StringH,
    'comments'                  => StringH,
    'content-description'       => StringH
  }

end


end   # module TMail