#
# loader.rb
#
#   Copyright (c) 1998-2001 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 'socket'
require 'final'
require 'amstd/futils'


module TMail

  class MhLoader

    include FileUtils

    def initialize( dname )
      @dirname = mustdir( dname )
      @oldnext = nil
      @lasttime = nil
      @verbose = false
    end

    attr :dirname

    def each_mail
      each_filename do |fname|
        yield FilePort.new( fname )
      end
      @lasttime = Time.now
    end

    alias each each_mail

    def new_mail
      FilePort.new nextfile
    end

    alias new_port new_mail

    def each_newmail( mtime = nil, &block )
      mtime ||= @lasttime
      unless mtime then
        return each_mail( &block )
      end

      return if mtime > File.mtime( @dirname )
      each_filename do |fname|
        if File.mtime( fname ) > mtime then
          yield FilePort.new( fname )
        end
      end
      @lasttime = Time.now
    end


    private

    def each_filename( &block )
      ret = []
      Dir.foreach( @dirname ) do |fn|
        path = "#{@dirname}/#{fn}"
        next unless /\A\d+\z/ === fn
        next unless file? path
        ret.push path
      end
      ret.each( &block )
    end

    def nextfile
      n = @oldnext
      unless n then
        n = 0
        each_filename do |fname, base|
          i = base.to_i
          n = i if i > n
        end
        n += 1
      end

      while exist? "#{@dirname}/#{n}" do
        n += 1
      end
      @oldnext = n

      "#{@dirname}/#{n}"
    end

  end



  class MboxLoader

    include FileUtils

    def initialize( fname, dir = nil )
      @filename = File.expand_path( fname )
      unless file? @filename then
        raise ArgumentError, "'#{fname}' is not normal file"
      end

      if dir then
        tmp = mustdir( dir )
      else
        tmp = isdir( "/tmp/tmail_mboxloader_#{$$}_#{id}" )
      end

      @closed = false
      @real = MhLoader.new( tmp )

      @finalizer = MboxLoader.mkfinal( @real, @filename )
      ObjectSpace.define_finalizer self, @finalizer
    end

    class << self
      def mkfinal( ld, mbfile )
        proc {
          File.open( mbfile, 'w' ) do |f|
            ld.each do |port|
              fr = get_fromline( port )
              port.ropen do |s|
                f.puts fr
                f.puts s.read_all
              end
            end
          end
          FILEUTILS.rm_rf ld.dirname
        }
      end

      def get_fromline( port )
        ad = 'nobody'
        if h = HeaderField.new_header( port, 'From' ) then
          if a = h.addrs[0] then
            ad = a.address
          end
        end

        t = File.mtime( port.filename )
        t = t.strftime( '%a %b %d %H:%M:%S %Y' )

        "From #{ad} #{t}"
      end
    end

    def close
      return if @closed
      @closed = true
      
      ObjectSpace.undefine_finalizer self
      @finalizer.call
      @finalizer = nil

      @real = nil
    end

    def each_mail( &block )
      close_check
      update
      @real.each_mail( &block )
    end

    alias each each_mail

    def each_newmail( mtime = nil )
      close_check
      update
      @real.each_newmail( mtime ) {|p| yield p }
    end

    def new_port
      close_check
      @real.new_port
    end

    alias new_mail new_port


    private

    FROMLINE = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/

    def update
      return if FileTest.zero? @filename
      wf = t = p = nil

      lock( @filename ) {|f|
        begin
          f.each do |line|
            if /\AFrom / === line then
              wf.close if wf
              File.utime t, t, p.filename if t

              p = @real.new_port
              wf = p.wopen
              if m = FROMLINE.match(line) then
                t = Time.local( m[6].to_i, m[1], m[2].to_i,
                                m[3].to_i, m[4].to_i, m[5].to_i )
              else
                t = nil
              end
            else
              wf << line if wf
            end
          end
        ensure
          if wf and not wf.closed? then
            wf.close
            File.utime t, t, p.filename if t
          end
        end
      }
      File.truncate @filename, 0
    end
  
    def lock( fname )
      begin
        f = File.open( fname )
        f.flock File::LOCK_EX
        yield f
      ensure
        f.flock File::LOCK_UN
        f.close
      end
    end

    def close_check
      if @closed then
        raise ArgumentError, 'accessing already closed mbox'
      end
    end

  end



  class MaildirLoader

    include FileUtils

    def initialize( dname = nil )
      dname ||= ENV['MAILDIR']
      @dirname = mustdir( dname )
      @new = mustdir( "#{dname}/new" )
      @tmp = mustdir( "#{dname}/tmp" )
      @cur = mustdir( "#{dname}/cur" )
    end

    def close
      @closed = true
    end


    def each_filename( dn )
      Dir.foreach( dn ) do |fn|
        full = "#{dn}/#{fn}"
        if fn[0] != ?. and File.file? full then
          yield full, fn
        end
      end
    end

    def each_mail
      each_filename( @tmp ) do |full, fname|
        yield FilePort.new( full )
      end
    end

    alias each each_mail

    def new_mail
      fn = nil
      tmp = nil
      i = 0

      while true do
        fn = "#{Time.now.to_i}.#{$$}.#{Socket.gethostname}"
        tmp = "#{@tmp}/#{fn}"
        break unless File.exist? tmp
        i += 1
        if i == 3 then
          raise IOError, "can't create new file in maildir"
        end
        sleep 1 #2
      end
      File.open( tmp, 'w' ){|f| f.write "\n\n" }
      cur = "#{@cur}/#{fn}"
      File.rename tmp, cur

      FilePort.new( cur )
    end

    alias new_port new_mail

    def each_newmail( &block )
      arr = []
      each_filename( @new ) do |old, fname|
        new = "#{@new}/#{fname}:2,"
        File.rename old, new
        arr.push FilePort.new( new )
      end
      arr.each( &block )

      check_tmp
    end


    TOO_OLD = 60 * 60 * 36   # 36 hour

    def check_tmp
      old = Time.now.to_i - TOO_OLD
      
      each_filename( @tmp ) do |full, fname|
        if File.file? full then
          if File.stat(full).mtime.to_i < nt then
            File.rm_f full
          end
        end
      end
    end
    
  end


end   # module TMail
