#!/usr/bin/env ruby
# $Id$
# Copyright (C) 2005  Shugo Maeda <shugo@ruby-lang.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

require "socket"
require "fcntl"
require "monitor"
require "net/imap"
require "pstore"
require "yaml/store"
require "find"
require "fileutils"
require "digest/md5"
require "iconv"
require "nkf"
require "date"
require "set"
require "logger"
require "timeout"
require "optparse"
require "sdbm"
require "rast"
require "rmail/parser"

now = DateTime.now
unless defined?(now.to_time)
  class DateTime
    def to_time
      d = new_offset(0)
      d.instance_eval {
        Time.utc(year, mon, mday, hour, min, sec,
                 (sec_fraction * 86400000000).to_i)
      }.getlocal
    end

    def to_datetime
      return self
    end
  end

  class Time
    def to_time
      return getlocal
    end

    def to_datetime
      jd = DateTime.civil_to_jd(year, mon, mday, DateTime::ITALY)
      fr = DateTime.time_to_day_fraction(hour, min, [sec, 59].min) +
           usec.to_r/86400000000
      of = utc_offset.to_r/86400
      DateTime.new0(DateTime.jd_to_ajd(jd, fr, of), of, DateTime::ITALY)
    end
  end
end

module Rast
  unless defined?(RESULT_ALL_ITEMS)
    if Rast::VERSION < "0.1.0"
      RESULT_ALL_ITEMS = 0
    else
      RESULT_ALL_ITEMS = -1
    end
  end
  if Rast::VERSION < "0.1.0"
    RESULT_MIN_ITEMS = 1
  else
    RESULT_MIN_ITEMS = 0
  end
end

class Ximapd
  attr_accessor :destination

  VERSION = "0.0.2"
  REVISION = "$Revision$".slice(/\A\$Revision: (.*) \$\z/, 1)
  DATE = "$Date$".slice(/\d\d\d\d-\d\d-\d\d/)

  LOG_SHIFT_AGE = 10
  LOG_SHIFT_SIZE = 1 * 1024 * 1024
  MAX_CLIENTS = 10
  TIMEZONE = Time.now.strftime("%z")

  @@debug = false

  def self.debug
    return @@debug
  end

  def self.debug=(debug)
    @@debug = debug
  end

  def initialize
    @args = nil
    @config = {}
    @server = nil
    @mail_store = nil
    @logger = nil
    @threads = []
    @sessions = {}
    @option_parser = OptionParser.new { |opts|
      opts.banner = "usage: ximapd [options]"
      opts.separator("")
      opts.separator("options:")
      define_options(opts, @config, OPTIONS)
      opts.separator("")
      define_options(opts, @config, ACTIONS)
    }
    @max_clients = nil
  end

  def run(args)
    begin
      @args = args
      parse_options(@args)
      @server = nil
      @logger = open_logger
      @config["logger"] = @logger
      @max_clients = @config["max_clients"] || MAX_CLIENTS
      @threads = []
      @sessions = {}
      send(@config["action"])
    rescue StandardError => e
      STDERR.printf("ximapd: %s\n", e)
      @logger.log_exception(e)
    end
  end

  private

  def define_options(option_parser, config, options)
    for option in options
      option.define(option_parser, config)
    end
  end

  def parse_options(args)
    begin
      @option_parser.parse!(args)
      @config["action"] ||= "help"
      if @config["action"] != "help" && @config["action"] != "version"
        config_file = File.expand_path(@config["config_file"] || "~/.ximapd")
        config = File.open(config_file) { |f|
          if f.stat.mode & 0004 != 0
            raise format("%s is world readable", config_file)
          end
          YAML.load(f)
        }
        @config = config.merge(@config)
        check_config(@config)
        path = @config["plugin_path"] ||
          File.join(@config["data_dir"], "plugins")
        Plugin.directories = path.split(File::PATH_SEPARATOR).collect { |dir|
          File.expand_path(dir)
        }
      end
    rescue StandardError => e
      raise if @config["debug"]
      STDERR.printf("ximapd: %s\n", e)
      exit(1)
    end
  end

  def open_logger
    if @config["debug"]
      logger = Logger.new(STDERR)
      logger.level = Logger::DEBUG
    else
      log_file = @config["log_file"] ||
        File.expand_path("ximapd.log", @config["data_dir"])
      FileUtils.mkdir_p(File.dirname(log_file))
      shift_age = @config["log_shift_age"] || LOG_SHIFT_AGE
      shift_size = @config["log_shift_size"] || LOG_SHIFT_SIZE
      logger = Logger.new(log_file, shift_age, shift_size)
      log_level = (@config["log_level"] || "INFO").upcase
      logger.level = Logger.const_get(log_level)
    end
    logger.datetime_format = "%Y-%m-%dT%H:%M:%S "
    if @config["action"] != "start"
      for m in [:fatal, :error, :warn]
        class << logger; self; end.send(:define_method, m) do |s|
          STDERR.printf("ximapd: %s\n", s)
          super(s)
        end
      end
      if @config["verbose"]
        def logger.info(s)
          puts(s)
          super(s)
        end
      end
    end
    def logger.log_exception(e, msg = nil, severity = Logger::ERROR)
      if msg
        add(severity, "#{msg}: #{e.class}: #{e.message}")
      else
        add(severity, "#{e.class}: #{e.message}")
      end
      for line in e.backtrace
        debug("  #{line}")
      end
    end
    return logger
  end

  def start
    @server = open_server
    @logger.info("started")
    daemon unless @config["debug"]
    @main_thread = Thread.current
    Signal.trap("TERM", &method(:terminate))
    Signal.trap("INT", &method(:terminate))
    begin
      @mail_store = Ximapd::MailStore.new(@config)
      begin
        loop do
          begin
            sock = @server.accept
          rescue Exception => e
            if @config["ssl"] && e.kind_of?(OpenSSL::SSL::SSLError)
              retry
            else
              raise
            end
          end
          unless @config["ssl"]
            sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
            if defined?(Fcntl::FD_CLOEXEC)
              sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) 
            end
            if defined?(Fcntl::O_NONBLOCK)
              sock.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
            end
          end
          if @sessions.length >= @max_clients
            sock.print("* BYE too many clients\r\n")
            peeraddr = sock.peeraddr[3]
            sock.close
            @logger.info("rejected connection from #{peeraddr}: " +
                         "too many clients")
            next
          end
          Thread.start(sock) do |socket|
            session = Session.new(@config, socket, @mail_store)
            @sessions[Thread.current] = session
            begin
              session.start
            ensure
              @sessions.delete(Thread.current)
            end
          end
        end
      ensure
        @mail_store.close
      end
    rescue SystemExit
      raise
    rescue Exception => e
      @logger.log_exception(e)
    ensure
      @logger.close
      @server.close
    end
  end

  def open_server
    server = TCPServer.new(@config["port"])
    if @config["ssl"]
      require "openssl"
      ssl_ctx = OpenSSL::SSL::SSLContext.new
      if @config.key?("ssl_key") && @config.key?("ssl_cert")
      ssl_ctx.key = File.open(@config["ssl_key"], 'r') { |f|
        OpenSSL::PKey::RSA.new(f)
      }
      ssl_ctx.cert = File.open(@config["ssl_cert"], 'r') { |f|
        OpenSSL::X509::Certificate.new(i)
      }
      else
        require 'webrick/ssl'
        ssl_ctx.cert, ssl_ctx.key =
          WEBrick::Utils::create_self_signed_cert(1024, [["CN", "Ximapd"]],
                                                  "Generated by Ruby/OpenSSL")
      end
      server = OpenSSL::SSL::SSLServer.new(server, ssl_ctx)
    end
    return server
  end

  def stop
    begin
      open_pid_file("r") do |f|
        pid = f.gets.to_i
        if pid != 0
          Process.kill("TERM", pid)
        end
      end
    rescue Errno::ENOENT
    end
  end

  def version
    printf("ximapd version %s (r%s %s)\n", VERSION, REVISION, DATE)
    puts
    printf("  Platform    : %s\n", RUBY_PLATFORM)
    printf("  Ruby        : %s (%s)\n", RUBY_VERSION, RUBY_RELEASE_DATE)
    printf("  Rast        : %s\n", Rast::VERSION)
    begin
      require "openssl"
      printf("  OpenSSL     : %s\n", OpenSSL::VERSION)
    rescue LoadError
      printf("  OpenSSL     : not available\n")
    end
    printf("  ProgressBar : %s\n", ProgressBar::VERSION)
  end

  def help
    puts(@option_parser.help)
  end

  def import
    open_mail_store do |mail_store|
      if @args.empty?
        mail_store.import_file(STDIN, @config["dest_mailbox"])
      else
        mail_store.import(@args, @config["dest_mailbox"])
      end
    end
  end

  def import_mbox
    open_mail_store do |mail_store|
      if @args.empty?
        mail_store.import_mbox_file(STDIN, @config["dest_mailbox"])
      else
        mail_store.import_mbox(@args, @config["dest_mailbox"])
      end
    end
  end

  def import_imap
    unless @config["remote_user"]
      print("user: ")
      @config["remote_user"] = STDIN.gets.chomp
    end
    unless @config["remote_password"]
      print("password: ")
      system("stty", "-echo")
      begin
        @config["remote_password"] = STDIN.gets.chomp
      ensure
        system("stty", "echo")
        puts
      end
    end
    open_mail_store do |mail_store|
      if ARGV.empty?
        args = ["INBOX"]
      else
        args = @args
      end
      mail_store.import_imap(args, @config["dest_mailbox"])
    end
  end

  def rebuild_index
    open_mail_store do |mail_store|
      mail_store.rebuild_index
    end
  end

  def interactive
    @mail_store = Ximapd::MailStore.new(@config)
    begin
      sock = ConsoleSocket.new
      session = Ximapd::Session.new(@config, sock, @mail_store, true)
      session.start
    ensure
      @mail_store.close
    end
  end

  def check_config(config)
    unless config.key?("data_dir")
      raise "data_dir is not specified"
    end
  end

  def daemon
    exit!(0) if fork
    Process.setsid
    exit!(0) if fork
    open_pid_file("w") do |f|
      f.puts(Process.pid)
    end
    Dir.chdir(File.expand_path(@config["data_dir"]))
    STDIN.reopen("/dev/null")
    STDOUT.reopen("/dev/null", "w")
    path = File.expand_path("stderr", @config["data_dir"])
    STDERR.reopen(path, "a")
  end

  def open_pid_file(mode = "r")
    pid_file = File.expand_path("pid", @config["data_dir"])
    File.open(pid_file, mode) do |f|
      yield(f)
    end
  end

  def close_sessions
    @logger.debug("close #{@sessions.length} sessions")
    i = 1
    @sessions.each_key do |t|
      @logger.debug("close session \##{i}")
      @mail_store.synchronize do
        @logger.debug("raise TerminateException to #{t}")
        t.raise(TerminateException.new)
        @logger.debug("raised TerminateException to #{t}")
      end
      begin
        @logger.debug("join #{t}")
        begin
          t.join
        rescue TerminateException
        end
      rescue SystemExit
        raise
      rescue Exception => e
        @logger.log_exception(e)
      ensure
        @logger.debug("joined #{t}")
      end
      @logger.debug("closed session \##{i}")
      i += 1
    end
  end

  def terminate(sig)
    @logger.info("signal #{sig}")
    @logger.debug("sessions: #{@sessions.keys.inspect}")
    close_sessions
    @logger.info("terminated")
    exit
  end

  def open_mail_store
    mail_store = Ximapd::MailStore.new(@config)
    begin
      mail_store.synchronize do
        if @config["dest_mailbox"] &&
          !mail_store.mailboxes.include?(@config["dest_mailbox"])
          mail_store.create_mailbox(@config["dest_mailbox"])
        end
        yield(mail_store)
      end
    ensure
      mail_store.close
    end
  end

  INDEX_OPTIONS = {
    "encoding" => "utf8",
    "preserve_text" => false,
    "properties" => [
      {
        "name" => "uid",
        "type" => Rast::PROPERTY_TYPE_UINT,
        "search" => true,
        "text_search" => false,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "size",
        "type" => Rast::PROPERTY_TYPE_UINT,
        "search" => true,
        "text_search" => false,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "internal-date",
        "type" => Rast::PROPERTY_TYPE_DATE,
        "search" => true,
        "text_search" => false,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "flags",
        "type" => Rast::PROPERTY_TYPE_STRING,
        "search" => false,
        "text_search" => true,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "mailbox-id",
        "type" => Rast::PROPERTY_TYPE_UINT,
        "search" => true,
        "text_search" => false,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "date",
        "type" => Rast::PROPERTY_TYPE_DATE,
        "search" => true,
        "text_search" => false,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "subject",
        "type" => Rast::PROPERTY_TYPE_STRING,
        "search" => false,
        "text_search" => true,
        "full_text_search" => true,
        "unique" => false,
      },
      {
        "name" => "from",
        "type" => Rast::PROPERTY_TYPE_STRING,
        "search" => false,
        "text_search" => true,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "to",
        "type" => Rast::PROPERTY_TYPE_STRING,
        "search" => false,
        "text_search" => true,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "cc",
        "type" => Rast::PROPERTY_TYPE_STRING,
        "search" => false,
        "text_search" => true,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "x-ml-name",
        "type" => Rast::PROPERTY_TYPE_STRING,
        "search" => true,
        "text_search" => false,
        "full_text_search" => false,
        "unique" => false,
      },
      {
        "name" => "x-mail-count",
        "type" => Rast::PROPERTY_TYPE_UINT,
        "search" => true,
        "text_search" => false,
        "full_text_search" => false,
        "unique" => false,
      }
    ]
  }

  DEFAULT_CHARSET = "iso-2022-jp"
  DEFAULT_SYNC_THRESHOLD_CHARS = 500000
  DEFAULT_ML_HEADER_FIELDS = [
    "x-ml-name",
    "list-id",
    "mailing-list"
  ]
  DEFAULT_MAILBOXES = {
    "INBOX" => {
      "flags" => "",
      "id" => 1,
      "query" => "mailbox-id = 1",
      "last_peeked_uid" => 0
    },
    "ml" => {
      "flags" => "\\Noselect"
    },
    "queries" => {
      "flags" => "\\Noselect"
    },
    "static" => {
      "flags" => "\\Noselect"
    }
  }

  module QueryFormat
    module_function

    def quote_query(s)
      return format('"%s"', s.gsub(/[\\"]/n, "\\\\\\&"))
    end
  end

  module DataFormat
    module_function

    def quoted(s)
      if s.nil?
        return "NIL"
      else
        return format('"%s"', s.to_s.gsub(/[\\"]/n, "\\\\\\&"))
      end
    end

    def literal(s)
      return format("{%d}\r\n%s", s.length, s)
    end
  end

  class MailData
    attr_reader :raw_data, :uid, :flags, :internal_date, :text, :properties

    def initialize(raw_data, uid, flags, internal_date, text, properties,
                   parsed_mail)
      @raw_data = raw_data
      @uid = uid
      @flags = flags
      @internal_date = internal_date
      @text = text
      @properties = properties
      @parsed_mail = parsed_mail
    end

    def to_s
      return @raw_data
    end

    def header
      return @parsed_mail.header
    end

    def multipart?
      return @parsed_mail.multipart?
    end

    def body
      return @parsed_mail.body
    end
  end

  class NullMessage
    attr_reader :header, :body

    def initialize
      @header = {}
      @body = ""
    end

    def multipart?
      return false
    end
  end

  MailboxStatus = Struct.new(:messages, :recent, :uidnext, :uidvalidity,
                             :unseen)

  class MailStore
    include QueryFormat
    include MonitorMixin

    attr_reader :config, :path, :mailbox_db, :flags_db, :plugins
    attr_reader :uid_seq, :uidvalidity_seq, :mailbox_id_seq

    def initialize(config)
      super()
      @config = config
      @logger = @config["logger"]
      @path = File.expand_path(@config["data_dir"])
      FileUtils.mkdir_p(@path)
      FileUtils.mkdir_p(File.expand_path("mails", @path))
      uid_seq_path = File.expand_path("uid.seq", @path)
      @uid_seq = Sequence.new(uid_seq_path)
      uidvalidity_seq_path = File.expand_path("uidvalidity.seq", @path)
      @uidvalidity_seq = Sequence.new(uidvalidity_seq_path)
      mailbox_id_seq_path = File.expand_path("mailbox_id.seq", @path)
      @mailbox_id_seq = Sequence.new(mailbox_id_seq_path)
      mailbox_db_path = File.expand_path("mailbox.db", @path)
      case @config["db_type"].to_s.downcase
      when "pstore"
        @mailbox_db = PStore.new(mailbox_db_path)
      else
        @mailbox_db = YAML::Store.new(mailbox_db_path)
      end
      @flags_db_path = File.expand_path("flags.sdbm", @path)
      @flags_db = nil
      @mail_parser = RMail::Parser.new
      @default_charset = @config["default_charset"] || DEFAULT_CHARSET
      @sync_threshold_chars =
        @config["sync_threshold_chars"] || DEFAULT_SYNC_THRESHOLD_CHARS
      @ml_header_fields =
        @config["ml_header_fields"] || DEFAULT_ML_HEADER_FIELDS
      @last_peeked_uids = {}
      @index_path = File.expand_path("index", @path)
      @index = nil
      @index_ref_count = 0
      lock_path = File.expand_path("lock", @path)
      @lock = File.open(lock_path, "w+")
      @lock_count = 0
      synchronize do
        if @uidvalidity_seq.current.nil?
          @uidvalidity_seq.current = 1
        end
        if @mailbox_id_seq.current.nil?
          @mailbox_id_seq.current = 1
        end
        @mailbox_db.transaction do
          @mailbox_db["mailboxes"] ||= DEFAULT_MAILBOXES.dup
          @mailbox_db["mailing-lists"] ||= {}
          convert_old_mailbox_db
          @plugins = Plugin.create_plugins(@config, self)
        end
        unless File.exist?(@index_path)
          Rast::DB.create(@index_path, INDEX_OPTIONS)
        end
      end
    end

    def close
      @lock.close
    end

    def lock
      mon_enter
      if @lock_count == 0
        @lock.flock(File::LOCK_EX)
        @flags_db = SDBM.open(@flags_db_path)
      end
      @lock_count += 1
    end

    def unlock
      @lock_count -= 1
      if @lock_count == 0 && !@lock.closed?
        @flags_db.close
        @flags_db = nil
        @lock.flock(File::LOCK_UN)
      end
      mon_exit
    end

    def synchronize
      lock
      begin
        yield
      ensure
        unlock
      end
    end

    def write_last_peeked_uids
      return if @last_peeked_uids.empty?
      @mailbox_db.transaction do
        @last_peeked_uids.each do |name, uid|
          mailbox = @mailbox_db["mailboxes"][name]
          if mailbox && mailbox["last_peeked_uid"] < uid
            mailbox["last_peeked_uid"] = uid
          end
        end
        @last_peeked_uids.clear
      end
    end

    def mailboxes
      @mailbox_db.transaction(true) do
        return @mailbox_db["mailboxes"]
      end
    end

    def create_mailbox(name, query = nil)
      @mailbox_db.transaction do
        dir = name.slice(/(.*)\/\z/ni, 1)
        if dir
          mkdir_p(dir)
        else
          mkdir_p(File.dirname(name))
          create_mailbox_internal(name, query)
        end
      end
    end

    def delete_mailbox(name)
      @mailbox_db.transaction do
        pat = "\\A" + Regexp.quote(name) + "(/.*)?\\z"
        re = Regexp.new(pat, nil, "n")
        @mailbox_db["mailboxes"].delete_if do |k, v|
          re.match(k)
        end
      end
    end

    def rename_mailbox(name, new_name)
      @mailbox_db.transaction do
        if @mailbox_db["mailboxes"].include?(new_name)
          raise format("%s already exists", new_name)
        end
        mkdir_p(File.dirname(new_name))
        pat = "\\A" + Regexp.quote(name) + "(/.*)?\\z"
        re = Regexp.new(pat, nil, "n")
        mailboxes = @mailbox_db["mailboxes"].select { |k, v|
          re.match(k)
        }
        for k, v in mailboxes
          new_key = k.sub(re) { $1 ? new_name + $1 : new_name }
          @mailbox_db["mailboxes"].delete(k)
          @mailbox_db["mailboxes"][new_key] = v
        end
        query = extract_query(new_name)
        if query
          @mailbox_db["mailboxes"][new_name]["query"] = query
        end
      end
    end

    def get_mailbox_status(mailbox_name, read_only)
      @mailbox_db.transaction(true) do
        mailbox = get_mailbox(mailbox_name)
        if /\\Noselect/ni.match(mailbox["flags"])
          raise NotSelectableMailboxError.new("can't open #{mailbox_name}: not a selectable mailbox")
        end
        mailbox_status = mailbox.status
        mailbox_status.uidnext = @uid_seq.peek_next
        mailbox_status.uidvalidity = @uidvalidity_seq.current
        unless read_only
          @last_peeked_uids[mailbox_name] = @uid_seq.current.to_i
        end
        return mailbox_status
      end
    end

    def import(args, mailbox_name = nil)
      open_index do |index|
        for arg in args
          filenames = []
          Find.find(arg) do |filename|
            if File.file?(filename)
              filenames.push(filename)
            end
          end
          if @config["progress"]
            progress_bar = ProgressBar.new(arg, filenames.length)
          else
            progress_bar = NullObject.new
          end
          for filename in filenames
            File.open(filename) do |f|
              import_mail_internal(f.read, mailbox_name, "", f.mtime)
            end
            progress_bar.inc
          end
          progress_bar.finish
        end
      end
    end

    def import_file(f, mailbox_name = nil)
      open_index do |index|
        return import_mail_internal(f.read, mailbox_name)
      end
    end

    def import_mbox(args, mailbox_name = nil)
      open_index do |index|
        for arg in args
          Find.find(arg) do |filename|
            if File.file?(filename)
              File.open(filename) do |f|
                import_mbox_internal(index, f, mailbox_name)
              end
            end
          end
        end
      end
    end

    def import_mbox_file(f, mailbox_name = nil)
      open_index do |index|
        import_mbox_internal(index, f, mailbox_name)
      end
    end

    def import_imap(folders, mailbox_name = nil)
      host = @config["remote_host"]
      port = @config["remote_port"]
      ssl = @config["remote_ssl"]
      if port.nil?
        if ssl
          port = 993
        else
          port = 143
        end
      end
      auth = @config["remote_auth"]
      if auth.nil?
        auth = "CRAM-MD5"
      end
      user = @config["remote_user"]
      pass = @config["remote_password"]

      @logger.info("importing from #{host} via IMAP...")
      imap = Net::IMAP.new(host, port, ssl)
      imap.authenticate(auth, user, pass)

      visited_folders = Set.new
      open_index do |index|
        folders.each do |f|
          folders = imap.list('', f)
          if folders.nil?
            @logger.warn("no folder matches: #{f}")
            next
          end
          folders.each do |folder|
            next if visited_folders.include?(folder.name)
            begin
              imap.select(folder.name)
              mail_count = imap.responses["EXISTS"][-1]
              if mail_count == 0
                @logger.info("0 messages in #{folder.name}")
                next
              end
              imported_uids = Set.new
              progress_bar = NullObject.new
              handler = Proc.new { |resp|
                if resp.kind_of?(Net::IMAP::UntaggedResponse) &&
                  resp.name == "FETCH"
                  mail = resp.data
                  # IMAP server may return only FLAGS if \Seen is set.
                  if !imported_uids.include?(mail.attr["UID"]) &&
                    mail.attr["BODY[]"]
                    indate = DateTime.strptime(mail.attr["INTERNALDATE"], 
                                               "%d-%b-%Y %H:%M:%S %z")
                    if @config["import_imap_flags"]
                      flags = mail.attr["FLAGS"].collect { |flag|
                        if flag.kind_of?(Symbol)
                          '\\' + flag.to_s
                        else
                          flag.to_s
                        end
                      }.join(" ")
                    else
                      flags = ""
                    end
                    import_mail_internal(mail.attr["BODY[]"], mailbox_name,
                                         flags, indate)
                    imported_uids.add(mail.attr["UID"])
                    progress_bar.inc
                    imap.responses["FETCH"].clear
                  end
                end
              }
              imap.add_response_handler(handler)
              begin
                fetch_attrs = ["UID", "BODY[]", "INTERNALDATE"]
                if @config["import_imap_flags"]
                  fetch_attrs.push("FLAGS")
                end
                if @config["import_all"]
                  @logger.info("#{mail_count} messages in #{folder.name}")
                  if @config["progress"]
                    progress_bar = ProgressBar.new(folder.name, mail_count)
                  end
                  imap.fetch(1 .. -1, fetch_attrs)
                else
                  uids = imap.uid_search(["UNSEEN"])
                  @logger.info("#{uids.length} unseen messages " + 
                               "in #{folder.name}")
                  if @config["progress"]
                    progress_bar = ProgressBar.new(folder.name, uids.length)
                  end
                  while uids.length > 0
                    imap.uid_fetch(uids.slice!(0, 100), fetch_attrs)
                  end
                end
                progress_bar.finish
              ensure
                imap.remove_response_handler(handler)
              end
              unless @config["keep"]
                uids = imported_uids.to_a.sort
                while uids.length > 0
                  imap.uid_store(uids.slice!(0, 100),
                                 "+FLAGS.SILENT", [:Deleted])
                end
              end
            rescue StandardError => e
              @logger.log_exception(e, folder.name)
            ensure
              visited_folders.add(folder.name)
              imap.close
            end
          end
        end
      end
      @logger.info("imported from #{host}")
    end

    def import_mail(str, mailbox_name = nil, flags = "", indate = nil)
      open_index do
        import_mail_internal(str, mailbox_name, flags, indate)
      end
    end

    def index_mail(mail)
      @index.register(mail.text, mail.properties)
      s = mail.properties["x-ml-name"]
      if !s.empty? && !@mailbox_db["mailing-lists"].key?(s)
        mbox = get_mailbox_name_from_x_ml_name(s)
        @mailbox_db["mailing-lists"][s] = mail.uid
        mailbox_name = format("ml/%s", Net::IMAP.encode_utf7(mbox))
        query = format("x-ml-name = %s",
                       quote_query(mail.properties["x-ml-name"]))
        begin
          create_mailbox_internal(mailbox_name, query)
        rescue MailboxExistError
        end
      end
    end

    def get_mailbox(name)
      if name == "DEFAULT"
        return DefaultMailbox.new(self)
      end
      data = @mailbox_db["mailboxes"][name]
      unless data
        raise NoMailboxError.new("no such mailbox")
      end
      class_name = data["class"]
      if class_name
        return Ximapd.const_get(class_name).new(self, name,
                                                @mailbox_db["mailboxes"][name])
      else
        return SearchBasedMailbox.new(self, name,
                                      @mailbox_db["mailboxes"][name])
      end
    end

    def delete_mails(mails)
      open_index do |index|
        for mail in mails
          for plugin in @plugins
            plugin.on_delete_mail(mail)
          end
          mail.delete
        end
      end
    end

    def open_index(flags = Rast::DB::RDWR)
      synchronize do
        if @index_ref_count == 0
          @index = Rast::DB.open(@index_path, flags,
                                 "sync_threshold_chars" =>
                                   @sync_threshold_chars)
        end
        @index_ref_count += 1
        begin
          yield(@index)
        ensure
          @index_ref_count -= 1
          if @index_ref_count == 0
            @index.close
            @index = nil
          end
        end
      end
    end

    def rebuild_index
      @logger.info("rebuilding index...")
      old_index_path = @index_path + ".old"
      begin
        File.rename(@index_path, old_index_path)
      rescue Errno::ENOENT
      end
      Rast::DB.create(@index_path, INDEX_OPTIONS)
      mailbox_names = {}
      @mailbox_db.transaction do
        for mailbox_name, mailbox_data in @mailbox_db["mailboxes"]
          id = mailbox_data["id"]
          if id
            mailbox_names[id] = mailbox_name
          end
        end
      end
      open_index do
        mail_dir = File.expand_path("mails", @path)
        Dir.glob(mail_dir + "/*/*").sort.each do |dir|
          reindex_month(dir)
        end
      end
      FileUtils.rm_rf(old_index_path)
      @logger.info("rebuilt index")
    end

    def get_next_mailbox_id
      return @mailbox_id_seq.next
    end

    private

    def convert_old_mailbox_db
      if @mailbox_db.root?("status")
        @uid_seq.current = @mailbox_db["status"]["last_uid"]
        @uidvalidity_seq.current = @mailbox_db["status"]["uidvalidity"]
        @mailbox_id_seq.current = @mailbox_db["status"]["last_mailbox_id"]
        @mailbox_db.delete("status")
      end
      @mailbox_db["mailboxes"]["static"] ||= {
        "flags" => "\\Noselect"
      }
    end

    def strip_unix_from(str, indate)
      str.sub!(/\AFrom\s+\S+\s+(.*)\r\n/) do
        if indate.nil?
          begin
            indate = DateTime.strptime($1 + " " + TIMEZONE,
                                       "%a %b %d %H:%M:%S %Y %z")
          rescue
          end
        end
        ""
      end
      return indate
    end

    def mkdir_p(dirname)
      if /\A\//n.match(dirname)
        raise "can't specify absolute path"
      end
      if dirname == "." ||
        @mailbox_db["mailboxes"].include?(dirname)
        return
      end
      mkdir_p(File.dirname(dirname))
      @mailbox_db["mailboxes"][dirname] = {
        "flags" => "\\Noselect"
      }
    end

    def import_mail_internal(str, mailbox_name = nil, flags = "", indate = nil)
      uid = get_next_uid
      mail = parse_mail(str, uid, flags, indate)
      @mailbox_db.transaction do
        if mailbox_name
          mailbox = get_mailbox(mailbox_name)
        else
          mailbox = nil
          for plugin in @plugins
            begin
              mbox_name = plugin.filter(mail)
              if mbox_name == "REJECT"
                @logger.add(Logger::INFO, "rejected: from=<#{mail.properties['from']}> subject=<#{mail.properties['subject']}> date=<#{mail.properties['date']}>")
                return 0
              end
              if mbox_name
                mailbox = get_mailbox(mbox_name)
                break
              end
            rescue Exception => e
              @logger.log_exception(e)
            end
          end
          mailbox ||= DefaultMailbox.new(self)
        end
        mailbox.import(mail)
        @logger.add(Logger::INFO, "imported: uid=#{mail.uid} from=<#{mail.properties['from']}> subject=<#{mail.properties['subject']}> date=<#{mail.properties['date']}> mailbox=<#{mailbox.name}>")
      end
      return mail.uid
    end

    def get_mailbox_name_from_x_ml_name(s)
      mbox = s.slice(/(.*) <.*>/u, 1) 
      if mbox.nil?
        mbox = s.slice(/<(.*)>/u, 1) 
        if mbox.nil?
          mbox = s.slice(/\S+@[^ \t;]+/u) 
          if mbox.nil?
            mbox = s
          end
        end
      end
      return mbox
    end

    def create_mailbox_internal(name, query = nil)
      if @mailbox_db["mailboxes"].key?(name)
        raise MailboxExistError, format("mailbox already exist - %s", name)
      end
      if /\Astatic\//u.match(name)
        mailbox = StaticMailbox.new(self, name, "flags" => "")
        mailbox.save
        return
      end
      mailbox = {
        "flags" => "",
        "last_peeked_uid" => 0
      }
      if query.nil?
        query = extract_query(name)
        if query.nil?
          mailbox_id = get_next_mailbox_id
          query = format('mailbox-id = %d', mailbox_id)
          mailbox["id"] = mailbox_id
        end
      end
      mailbox["query"] = query
      @mailbox_db["mailboxes"][name] = mailbox
    end

    def extract_query(mailbox_name)
      s = mailbox_name.slice(/\Aqueries\/(.*)/u, 1)
      return nil if s.nil?
      query = Net::IMAP.decode_utf7(s)
      begin
        open_index do |index|
          result = index.search(query, "num_items" => Rast::RESULT_MIN_ITEMS)
        end
        return query
      rescue
        raise InvalidQueryError.new("invalid query")
      end
    end

    def import_mbox_internal(index, f, mailbox_name = nil)
      s = nil
      f.each_line do |line|
        if /\AFrom\s+\S+\s+[A-Z][a-z]{2} [A-Z][a-z]{2}\s+\d+ \d\d:\d\d:\d\d \d+/.match(line)
          if s
            uid = import_mail_internal(s, mailbox_name)
          end
          s = line
        else
          s.concat(line) if s
        end
      end
      if s
        uid = import_mail_internal(s, mailbox_name)
      end
    end

    def reindex_month(dir)
      filenames = []
      Find.find(dir) do |filename|
        if File.file?(filename) && /\/\d+\z/.match(filename)
          filenames.push(filename)
        end
      end
      if @config["progress"]
        month = dir.slice(/\d+\/\d+\z/)
        progress_bar = ProgressBar.new(month, filenames.length)
      else
        progress_bar = NullObject.new
      end
      for filename in filenames
        reindex_mail(filename)
        progress_bar.inc
      end
      progress_bar.finish
    end

    def reindex_mail(filename)
      begin
        str = File.read(filename)
        uid = filename.slice(/\/(\d+)\z/, 1).to_i
        flags = @flags_db[uid.to_s] || "\\Seen"
        indate = File.mtime(filename)
        mail = parse_mail(str, uid, flags, indate)
        begin
          mail.properties["mailbox-id"] =
            File.read(filename + ".mailbox-id").to_i
        rescue Errno::ENOENT
        end
        @mailbox_db.transaction do
          index_mail(mail)
        end
      rescue StandardError => e
        @logger.log_exception(e)
      end
    end

    def parse_mail(mail, uid, flags, indate)
      mail.gsub!(/\r?\n/, "\r\n")
      indate = strip_unix_from(mail, indate)
      if indate.nil?
        indate = DateTime.now
      end
      properties = Hash.new("")
      properties["uid"] = uid
      properties["size"] = mail.size
      properties["flags"] = ""
      properties["internal-date"] = indate.strftime("%Y-%m-%dT%H:%M:%S")
      properties["date"] = properties["internal-date"]
      properties["x-mail-count"] = 0
      properties["mailbox-id"] = 0
      begin
        m = @mail_parser.parse(mail.gsub(/\r\n/, "\n"))
        properties = extract_properties(m, properties)
        body = extract_body(m)
      rescue Exception => e
        @logger.log_exception(e, "failed to parse mail uid=#{uid}",
                              Logger::WARN)
        header, body = *mail.split(/^\r\n/)
        body = to_utf8(body, @default_charset)
      end
      return MailData.new(mail, uid, flags, indate, body, properties,
                          m || NullMessage.new)
    end

    def get_mailbox_id(mailbox_name)
      if mailbox_name.nil?
        mailbox_id = 0
      else
        mailbox = @mailbox_db["mailboxes"][mailbox_name]
        if mailbox.nil?
          raise NoMailboxError.new("no such mailbox")
        end
        mailbox_id = mailbox["id"]
        if mailbox_id.nil?
          raise MailboxAccessError.new("can't import to mailbox without id")
        end
      end
      return mailbox_id
    end

    def get_next_uid
      return @uid_seq.next
    end

    def extract_body(mail)
      if mail.multipart?
        return mail.body.collect { |part|
          extract_body(part)
        }.join("\n")
      else
        case mail.header.content_type("text/plain")
        when "text/plain"
          return decode_body(mail)
        when "text/html", "text/xml"
          return decode_body(mail).gsub(/<.*?>/um, "")
        else
          return ""
        end
      end
    end

    def decode_body(mail)
      charset = mail.header.params("content-type", {})["charset"] ||
        @default_charset
      return to_utf8(mail.decode, charset)
    end

    def to_utf8(src, charset)
      begin
        return Iconv.conv("utf-8", charset, src)
      rescue
        return NKF.nkf("-m0 -w", src)
      end
    end

    def extract_properties(mail, properties)
      for field in ["subject", "from", "to", "cc"]
        properties[field] = get_header_field(mail, field)
      end
      begin
        properties["date"] = DateTime.parse(mail.header["date"].to_s).to_s
      rescue Exception => e
        @logger.log_exception(e, "failed to parse Date", Logger::WARN)
      end
      s = nil
      @ml_header_fields.each do |field_name|
        s ||= mail.header[field_name]
      end
      properties["x-ml-name"] = decode_encoded_word(s.to_s)
      properties["x-mail-count"] = mail.header["x-mail-count"].to_s.to_i
      return properties
    end

    def get_header_field(mail, field)
      return decode_encoded_word(mail.header[field].to_s)
    end

    def decode_encoded_word(s)
      return NKF.nkf("-w", s)
    end
  end

  class Mailbox
    attr_reader :mail_store, :name

    def initialize(mail_store, name, data)
      @mail_store = mail_store
      @name = name
      @data = data
      @config = mail_store.config
    end

    def [](key)
      return @data[key]
    end

    def save
      @data["class"] = self.class.name.slice(/\AXimapd::(.*)\z/, 1)
      @mail_store.mailbox_db["mailboxes"][@name] = @data
    end

    def import(mail_data)
      raise ScriptError.new("subclass must override Mailbox#import")
    end

    def get_mail_path(mail)
      raise ScriptError.new("subclass must override Mailbox#get_mail_path")
    end

    def status
      raise ScriptError.new("subclass must override Mailbox#status")
    end

    def uid_search(query)
      raise ScriptError.new("subclass must override Mailbox#uid_search")
    end

    def fetch(sequence_set)
      raise ScriptError.new("subclass must override Mailbox#fetch")
    end

    def uid_fetch(sequence_set)
      raise ScriptError.new("subclass must override Mailbox#uid_fetch")
    end
  end

  class SearchBasedMailbox < Mailbox
    def import(mail_data)
      if @data.key?("id")
        mail_data.properties["mailbox-id"] = @data["id"]
      end
      path = get_mail_path(mail_data)
      FileUtils.mkdir_p(File.dirname(path))
      File.open(path, "w") do |f|
        f.flock(File::LOCK_EX)
        f.print(mail_data)
      end
      mailbox_id_path = path + ".mailbox-id"
      File.open(mailbox_id_path, "w") do |f|
        f.flock(File::LOCK_EX)
        f.print(mail_data.properties["mailbox-id"].to_s)
      end
      time = mail_data.internal_date.to_time
      File.utime(time, time, path)
      @mail_store.flags_db[mail_data.uid.to_s] = mail_data.flags
      @mail_store.index_mail(mail_data)
    end

    def get_mail_path(mail)
      relpath = format("mails/%s/%d",
                       mail.internal_date.strftime("%Y/%m/%d"),
                       mail.uid)
      return File.expand_path(relpath, @mail_store.path)
    end

    def status
      @mail_store.open_index do |index|
        mailbox_status = MailboxStatus.new
        result = index.search(self["query"],
                              "properties" => ["uid"],
                              "start_no" => 0)
        mailbox_status.messages = result.hit_count
        mailbox_status.unseen = result.items.select { |i|
          !/\\Seen\b/ni.match(@mail_store.flags_db[i.properties[0].to_s])
        }.length
        query = format("%s uid > %d",
                       self["query"], self["last_peeked_uid"])
        result = index.search(query,
                              "properties" => ["uid"],
                              "start_no" => 0,
                              "num_items" => Rast::RESULT_MIN_ITEMS)
        mailbox_status.recent = result.hit_count
        return mailbox_status
      end
    end

    def uid_search(query)
      @mail_store.open_index do |index|
        options = {
          "properties" => ["uid"],
          "start_no" => 0,
          "num_items" => Rast::RESULT_ALL_ITEMS,
          "sort_method" => Rast::SORT_METHOD_PROPERTY,
          "sort_property" => "uid",
          "sort_order" => Rast::SORT_ORDER_ASCENDING
        }
        query += " " + self["query"]
        result = index.search(query, options)
        return result.items.collect { |i| i.properties[0] }
      end
    end

    def fetch(sequence_set)
      @mail_store.open_index do |index|
        result = index.search(self["query"],
                              "properties" => ["uid", "internal-date"],
                              "start_no" => 0,
                              "num_items" => Rast::RESULT_ALL_ITEMS,
                              "sort_method" => Rast::SORT_METHOD_PROPERTY,
                              "sort_property" => "uid",
                              "sort_order" => Rast::SORT_ORDER_ASCENDING)
        mails = []
        sequence_set.each do |seq_number|
          case seq_number
          when Range
            first = seq_number.first
            last = seq_number.last == -1 ? result.items.length : seq_number.last
            for i in first .. last
              item = result.items[i - 1]
              mail = IndexedMail.new(@config, self, i, item.properties[0],
                                     item.doc_id, item.properties[1])
              mails.push(mail)
            end
          else
            item = result.items[seq_number - 1]
            next if item.nil?
            mail = IndexedMail.new(@config, self, seq_number,
                                   item.properties[0], item.doc_id,
                                   item.properties[1])
            mails.push(mail)
          end
        end
        return mails
      end
    end

    def uid_fetch(sequence_set)
      @mail_store.open_index do |index|
        options = {
          "properties" => ["uid", "internal-date"],
          "start_no" => 0,
          "num_items" => Rast::RESULT_ALL_ITEMS,
          "sort_method" => Rast::SORT_METHOD_PROPERTY,
          "sort_property" => "uid",
          "sort_order" => Rast::SORT_ORDER_ASCENDING
        }
        additional_queries = sequence_set.collect { |seq_number|
          case seq_number
          when Range
            q = ""
            if seq_number.first > 1
              q += format(" uid >= %d", seq_number.first)
            end
            if seq_number.last != -1
              q += format(" uid <= %d", seq_number.last)
            end
            q
          else
            format("uid = %d", seq_number)
          end
        }.reject { |q| q.empty? }
        if additional_queries.empty?
          query = self["query"]
        else
          query = self["query"] +
            " ( " + additional_queries.join(" | ") + " )"
        end
        result = index.search(query, options)
        return result.items.collect { |i|
          uid = i.properties[0]
          IndexedMail.new(@config, self, uid, uid, i.doc_id, i.properties[1])
        }
      end
    end
  end

  class DefaultMailbox < SearchBasedMailbox
    def initialize(mail_store)
      super(mail_store, "DEFAULT", {})
    end

    def import(mail_data)
      if mail_data.properties["x-ml-name"].empty?
        mail_data.properties["mailbox-id"] = 1
      else
        mail_data.properties["mailbox-id"] = 0
      end
      super(mail_data)
    end
  end

  class StaticMailbox < Mailbox
    def initialize(mail_store, name, data)
      super(mail_store, name, data)
      relpath = format("mailboxes/%d/flags.sdbm", data["id"])
      @flags_db_path = File.expand_path(relpath, mail_store.path)
    end

    def save
      unless @mail_store.mailbox_db["mailboxes"].key?(@name)
        @data["id"] = @mail_store.get_next_mailbox_id
        @data["last_peeked_uid"] ||= 0
      end
      super
      FileUtils.mkdir_p(get_mailbox_dir)
    end

    def import(mail_data)
      path = get_mail_path(mail_data)
      FileUtils.mkdir_p(File.dirname(path))
      File.open(path, "w") do |f|
        f.flock(File::LOCK_EX)
        f.print(mail_data)
      end
      time = mail_data.internal_date.to_time
      File.utime(time, time, path)
      open_flags_db do |db|
        db[mail_data.uid.to_s] = mail_data.flags
      end
    end

    def get_mail_path(mail)
      return File.expand_path(mail.uid.to_s, get_mailbox_dir)
    end

    def status
      mailbox_status = MailboxStatus.new
      uids = get_uids
      mailbox_status.messages = uids.length
      open_flags_db do |db|
        mailbox_status.unseen = uids.select { |uid|
          !/\\Seen\b/ni.match(db[uid.to_s])
        }.length
      end
      mailbox_status.recent = uids.select { |uid|
        uid > self["last_peeked_uid"]
      }.length
      return mailbox_status
    end

    def uid_search(query)
      return []
    end

    def fetch(sequence_set)
      uids = get_uids
      mails = []
      sequence_set.each do |seq_number|
        case seq_number
        when Range
          first = seq_number.first
          last = seq_number.last == -1 ? uids.length : seq_number.last
          for i in first .. last
            uid = uids[i - 1]
            next if uid.nil?
            mail = StaticMail.new(@config, self, i, uid)
            mails.push(mail)
          end
        else
          uid = uids[seq_number - 1]
          next if uid.nil?
          mail = StaticMail.new(@config, self, seq_number, uid)
          mails.push(mail)
        end
      end
      return mails
    end

    def uid_fetch(sequence_set)
      uids = get_uids
      return uids if uids.empty?
      uid_set = Set.new(uids)
      mails = []
      sequence_set.each do |seq_number|
        case seq_number
        when Range
          first = seq_number.first
          last = seq_number.last == -1 ? uids.last : seq_number.last
          if last > uids.last
            last = uids.last
          end

          for uid in (first..last).to_a & uids
            mail = StaticMail.new(@config, self, uid, uid)
            mails.push(mail)
          end
        else
          next unless uid_set.include?(seq_number)
          mail = StaticMail.new(@config, self, seq_number, seq_number)
          mails.push(mail)
        end
      end
      return mails
    end

    def open_flags_db(&block)
      SDBM.open(@flags_db_path, &block)
    end

    private

    def get_mailbox_dir
      relpath = format("mailboxes/%d", self["id"])
      return File.expand_path(relpath, @mail_store.path)
    end

    def get_uids
      dirpath = File.expand_path(format("mailboxes/%d", self["id"]),
                                 @mail_store.path)
      return Dir.open(dirpath) { |dir|
        dir.grep(/\A\d+\z/).collect { |uid| uid.to_i }.sort
      }
    end
  end

  class Mail
    include DataFormat

    attr_reader :mailbox, :seqno, :uid

    def initialize(config, mailbox, seqno, uid)
      @config = config
      @mailbox = mailbox
      @seqno = seqno
      @uid = uid
      @mail_store = @mailbox.mail_store
      @parsed_mail = nil
    end

    def envelope
      mail = parsed_mail
      s = "("
      s.concat(quoted(mail.header["date"]))
      s.concat(" ")
      s.concat(quoted(mail.header["subject"]))
      s.concat(" ")
      s.concat(envelope_addrs(mail.header.from))
      s.concat(" ")
      s.concat(envelope_addrs(mail.header.from))
      s.concat(" ")
      if mail.header.reply_to.empty?
        s.concat(envelope_addrs(mail.header.from))
      else
        s.concat(envelope_addrs(mail.header.reply_to))
      end
      s.concat(" ")
      s.concat(envelope_addrs(mail.header.to))
      s.concat(" ")
      s.concat(envelope_addrs(mail.header.cc))
      s.concat(" ")
      s.concat(envelope_addrs(mail.header.bcc))
      s.concat(" ")
      s.concat(quoted(mail.header["in-reply-to"]))
      s.concat(" ")
      s.concat(quoted(mail.header["message-id"]))
      s.concat(")")
      return s
    end

    def path
      return @mailbox.get_mail_path(self)
    end

    def old_path
      dir1, dir2 = *[@uid].pack("v").unpack("H2H2")
      relpath = format("mail/%s/%s/%d", dir1, dir2, uid)
      return File.expand_path(relpath, @config["data_dir"])
    end

    def size
      begin
        return File.size(path)
      rescue Errno::ENOENT
        return File.size(old_path)
      end
    end

    def to_s
      open_file do |f|
        f.flock(File::LOCK_SH)
        return f.read
      end
    end

    def header
      open_file do |f|
        f.flock(File::LOCK_SH)
        return f.gets("\r\n\r\n")
      end
    end

    def header_fields(fields)
      pat = "^(?:" + fields.collect { |field|
        Regexp.quote(field)
      }.join("|") + "):.*(?:\r\n[ \t]+.*)*\r\n"
      re = Regexp.new(pat, true, "n")
      return header.scan(re).join + "\r\n"
    end

    def body
      return body_internal(parsed_mail)
    end

    def multipart?
      mail = parsed_mail
      return mail.multipart?
    end

    def delete
      begin
        File.unlink(path)
      rescue Errno::ENOENT
        File.unlink(old_path)
      end
    end

    private

    def open_file(mode = "r")
      begin
        File.open(path, mode) do |f|
          yield(f)
        end
      rescue Errno::ENOENT
        File.open(old_path, mode) do |f|
          yield(f)
        end
      end
    end

    def parsed_mail
      if @parsed_mail.nil?
        @parsed_mail = RMail::Parser.read(to_s.gsub(/\r\n/, "\n"))
      end
      return @parsed_mail
    end

    def envelope_addrs(addrs)
      if addrs.nil? || addrs.empty?
        return "NIL"
      else
        return "(" + addrs.collect { |addr|
          envelope_addr(addr)
        }.join(" ") + ")"
      end
    end

    def envelope_addr(addr)
      name = addr.display_name
      adl = nil
      if addr.local
        mailbox = addr.local.tr('"', '')
      else
        mailbox = nil
      end
      host = addr.domain
      return format("(%s %s %s %s)",
                    quoted(name), quoted(adl), quoted(mailbox), quoted(host))
    end

    def body_internal(mail)
      if mail.multipart?
        parts = mail.body.collect { |part|
          body_internal(part)
        }.join
        return format("(%s %s)", parts, quoted(upcase(mail.header.subtype)))
      else
        fields = []
        params = "(" + mail.header.params("content-type", {}).collect { |k, v|
            format("%s %s", quoted(upcase(k)), quoted(upcase(v)))
        }.join(" ") + ")"
        fields.push(params)
        fields.push("NIL")
        fields.push("NIL")
        content_transfer_encoding =
          (mail.header["content-transfer-encoding"] || "7BIT").to_s.upcase
        fields.push(quoted(content_transfer_encoding))
        fields.push(mail.body.gsub(/\n/, "\r\n").length.to_s)
        if mail.header.media_type == "text"
          fields.push(mail.body.to_a.length.to_s)
        end
        return format("(%s %s %s)",
                      quoted(upcase(mail.header.media_type)),
                      quoted(upcase(mail.header.subtype)),
                      fields.join(" "))
      end
    end

    def upcase(s)
      if s.nil?
        return s
      end
      return s.upcase
    end
  end

  class IndexedMail < Mail
    attr_reader :doc_id, :internal_date

    def initialize(config, mailbox, seqno, uid, doc_id, internal_date)
      super(config, mailbox, seqno, uid)
      @doc_id = doc_id
      @internal_date = DateTime.strptime(internal_date[0, 19] + " " + TIMEZONE,
                                         "%Y-%m-%dT%H:%M:%S %z")
    end

    def flags(get_recent = true)
      s = @mail_store.flags_db[@uid.to_s].to_s
      if get_recent && uid > @mailbox["last_peeked_uid"]
        if s.empty?
          return "\\Recent"
        else
          return "\\Recent " + s
        end
      else
        return s
      end
    end

    def flags=(s)
      @mail_store.flags_db[@uid.to_s] = s
    end

    def delete
      super
      @mail_store.flags_db.delete(@uid.to_s)
      @mail_store.open_index do |index|
        index.delete(@doc_id)
      end
    end
  end

  class StaticMail < Mail
    def internal_date
      return File.mtime(path)
    end

    def flags(get_recent = true)
      @mailbox.open_flags_db do |db|
        s = db[@uid.to_s].to_s
        if get_recent && uid > @mailbox["last_peeked_uid"]
          if s.empty?
            return "\\Recent"
          else
            return "\\Recent " + s
          end
        else
          return s
        end
      end
    end

    def flags=(s)
      @mailbox.open_flags_db do |db|
        db[@uid.to_s] = s
      end
    end

    def delete
      super
      @mailbox.open_flags_db do |db|
        db.delete(@uid.to_s)
      end
    end
  end

  class Sequence
    def initialize(path, initial_value = 1)
      @path = path
      @new_path = path + ".new"
      @tmp_path = path + ".tmp"
      @initial_value = initial_value
    end

    def current
      File.open(@path, File::RDWR | File::CREAT) do |f|
        f.flock(File::LOCK_SH)
        return get_current_value(f)
      end
    end

    def current=(value)
      File.open(@path, File::RDWR | File::CREAT) do |f|
        f.flock(File::LOCK_EX)
        write_value(f, value)
        return value
      end
    end

    def next
      File.open(@path, File::RDWR | File::CREAT) do |f|
        f.flock(File::LOCK_EX)
        value = get_next_value(f)
        write_value(f, value)
        return value
      end
    end

    def peek_next
      File.open(@path, File::RDWR | File::CREAT) do |f|
        f.flock(File::LOCK_SH)
        return get_next_value(f)
      end
    end

    private

    def get_current_value(f)
      begin
        s = File.read(@new_path)
        File.unlink(@new_path)
      rescue Errno::ENOENT
        s = f.read
      end
      if s.empty?
        return nil
      end
      return s.to_i
    end

    def get_next_value(f)
      n = get_current_value(f)
      if n.nil?
        return @initial_value
      end
      return n + 1
    end

    def write_value(f, value)
      File.open(@tmp_path, "w") do |tmp|
        tmp.print(value.to_s)
      end
      File.rename(@tmp_path, @new_path)
      f.rewind
      f.print(value.to_s)
      f.truncate(f.pos)
      File.unlink(@new_path)
    end
  end

  NON_AUTHENTICATED_STATE = :NON_AUTHENTICATED_STATE
  AUTHENTICATED_STATE = :AUTHENTICATED_STATE
  SELECTED_STATE = :SELECTED_STATE
  LOGOUT_STATE = :LOGOUT_STATE

  class Session
    attr_reader :config, :state, :mail_store, :current_mailbox, :peeraddr

    MAX_IDLE_SECONDS = 30 * 60

    @@test = false

    def self.test
      return @@test
    end

    def self.test=(test)
      @@test = test
    end

    def initialize(config, sock, mail_store, pre_authenticated = false)
      @config = config
      @sock = sock
      @mail_store = mail_store
      @pre_authenticated = pre_authenticated
      @logger = @config["logger"]
      @parser = CommandParser.new(self, @logger)
      @logout = false
      @peeraddr = nil
      if pre_authenticated
        @state = AUTHENTICATED_STATE
      else
        @state = NON_AUTHENTICATED_STATE
      end
      @current_mailbox = nil
      @read_only = false
    end

    def start
      synchronize do
        @peeraddr = @sock.peeraddr[3]
        @logger.info("connect from #{@peeraddr}")
        if @pre_authenticated
          send_preauth("ximapd version %s (r%s %s)", VERSION, REVISION, DATE)
        else
          send_ok("ximapd version %s (r%s %s)", VERSION, REVISION, DATE)
        end
      end
      begin
        while !@logout
          begin
            cmd = recv_cmd
          rescue StandardError => e
            send_bad("parse error: %s", e)
            next
          end
          break if cmd.nil?
          @logger.debug("received #{cmd.name} command from #{@peeraddr}")
          begin
            cmd.exec
          rescue StandardError => e
            raise if @@test
            send_tagged_no(cmd.tag, "%s failed - %s", cmd.name, e)
            @logger.log_exception(e)
          end
          @logger.debug("sent #{cmd.name} response to #{@peeraddr}")
        end
      rescue Timeout::Error
        @logger.info("autologout #{@peeraddr}")
        send_data("BYE Autologout; idle for too long")
      rescue TerminateException
        send_data("BYE IMAP server terminating connection")
      end
      synchronize do
        sync
        @sock.close
        @logger.info("disconnect from #{@peeraddr}")
      end
    end

    def logout
      @state = LOGOUT_STATE
      @logout = true
    end

    def login
      @state = AUTHENTICATED_STATE
    end

    def select(mailbox)
      @current_mailbox = mailbox
      @read_only = false
      @state = SELECTED_STATE
    end

    def examine(mailbox)
      @current_mailbox = mailbox
      @read_only = true
      @state = SELECTED_STATE
    end

    def get_current_mailbox
      @mail_store.mailbox_db.transaction do
        return @mail_store.get_mailbox(@current_mailbox)
      end
    end

    def read_only?
      return @read_only
    end

    def close_mailbox
      @current_mailbox = nil
      @state = AUTHENTICATED_STATE
    end

    def sync
      @mail_store.write_last_peeked_uids
    end

    def recv_line
      timeout(MAX_IDLE_SECONDS) do
        s = @sock.gets
        return s if s.nil?
        line = s.sub(/\r\n\z/n, "")
        @logger.debug(line.gsub(/^/n, "C: ")) if @config["debug"]
        return line
      end
    end

    def recv_cmd
      timeout(MAX_IDLE_SECONDS) do
        buf = ""
        loop do
          s = @sock.gets
          break unless s
          s.gsub!(/\r?\n\z/, "\r\n")
          buf.concat(s)
          if len = s.slice(/\{(\d+)\}\r\n/n, 1)
            send_continue_req("Ready for additional command text")
            n = len.to_i
            while n > 0
              tmp = @sock.read(n)
              n -= tmp.length
              buf.concat(tmp)
            end
          else
            break
          end
        end
        return nil if buf.length == 0
        @logger.debug(buf.gsub(/^/n, "C: ")) if @config["debug"]
        return @parser.parse(buf)
      end
    end

    def send_line(line)
      @logger.debug(line.gsub(/^/n, "S: ")) if @config["debug"]
      @sock.print(line + "\r\n")
    end

    def send_tagged_response(tag, name, fmt, *args)
      msg = format(fmt, *args)
      send_line(tag + " " + name + " " + msg)
    end

    def send_tagged_ok(tag, fmt, *args)
      send_tagged_response(tag, "OK", fmt, *args)
    end

    def send_tagged_no(tag, fmt, *args)
      send_tagged_response(tag, "NO", fmt, *args)
    end

    def send_tagged_bad(tag, fmt, *args)
      send_tagged_response(tag, "BAD", fmt, *args)
    end

    def send_data(fmt, *args)
      s = format(fmt, *args)
      send_line("* " + s)
    end

    def send_ok(fmt, *args)
      send_data("OK " + fmt, *args)
    end

    def send_no(fmt, *args)
      send_data("NO " + fmt, *args)
    end

    def send_bad(fmt, *args)
      send_data("BAD " + fmt, *args)
    end

    def send_preauth(fmt, *args)
      send_data("PREAUTH " + fmt, *args)
    end

    def send_continue_req(fmt, *args)
      msg = format(fmt, *args)
      send_line("+ " + msg)
    end

    def synchronize(&block)
      @mail_store.synchronize(&block)
    end
  end

  class CommandParser
    def initialize(session, logger)
      @session = session
      @logger = logger
      @str = nil
      @pos = nil
      @lex_state = nil
      @token = nil
    end

    def parse(str)
      @str = str
      @pos = 0
      @lex_state = EXPR_BEG
      @token = nil
      return command
    end

    private

    EXPR_BEG          = :EXPR_BEG
    EXPR_DATA         = :EXPR_DATA
    EXPR_TEXT         = :EXPR_TEXT
    EXPR_RTEXT        = :EXPR_RTEXT
    EXPR_CTEXT        = :EXPR_CTEXT

    T_SPACE   = :SPACE
    T_NIL     = :NIL
    T_NUMBER  = :NUMBER
    T_ATOM    = :ATOM
    T_QUOTED  = :QUOTED
    T_LPAR    = :LPAR
    T_RPAR    = :RPAR
    T_BSLASH  = :BSLASH
    T_STAR    = :STAR
    T_LBRA    = :LBRA
    T_RBRA    = :RBRA
    T_LITERAL = :LITERAL
    T_PLUS    = :PLUS
    T_PERCENT = :PERCENT
    T_CRLF    = :CRLF
    T_EOF     = :EOF
    T_TEXT    = :TEXT

    BEG_REGEXP = /\G(?:\
(?# 1:  SPACE   )( )|\
(?# 2:  NIL     )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
(?# 3:  NUMBER  )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\
(?# 4:  ATOM    )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\
(?# 5:  QUOTED  )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
(?# 6:  LPAR    )(\()|\
(?# 7:  RPAR    )(\))|\
(?# 8:  BSLASH  )(\\)|\
(?# 9:  STAR    )(\*)|\
(?# 10: LBRA    )(\[)|\
(?# 11: RBRA    )(\])|\
(?# 12: LITERAL )\{(\d+)\}\r\n|\
(?# 13: PLUS    )(\+)|\
(?# 14: PERCENT )(%)|\
(?# 15: CRLF    )(\r\n)|\
(?# 16: EOF     )(\z))/ni

    DATA_REGEXP = /\G(?:\
(?# 1:  SPACE   )( )|\
(?# 2:  NIL     )(NIL)|\
(?# 3:  NUMBER  )(\d+)|\
(?# 4:  QUOTED  )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\
(?# 5:  LITERAL )\{(\d+)\}\r\n|\
(?# 6:  LPAR    )(\()|\
(?# 7:  RPAR    )(\)))/ni

    TEXT_REGEXP = /\G(?:\
(?# 1:  TEXT    )([^\x00\r\n]*))/ni

    RTEXT_REGEXP = /\G(?:\
(?# 1:  LBRA    )(\[)|\
(?# 2:  TEXT    )([^\x00\r\n]*))/ni

    CTEXT_REGEXP = /\G(?:\
(?# 1:  TEXT    )([^\x00\r\n\]]*))/ni

    Token = Struct.new(:symbol, :value)

    UNIVERSAL_COMMANDS = [
      "CAPABILITY",
      "NOOP",
      "LOGOUT"
    ]
    NON_AUTHENTICATED_STATE_COMMANDS = UNIVERSAL_COMMANDS + [
      "AUTHENTICATE",
      "LOGIN"
    ]
    AUTHENTICATED_STATE_COMMANDS = UNIVERSAL_COMMANDS + [
      "SELECT",
      "EXAMINE",
      "CREATE",
      "DELETE",
      "RENAME",
      "SUBSCRIBE",
      "UNSUBSCRIBE",
      "LIST",
      "LSUB",
      "STATUS",
      "APPEND",
      "IDLE"
    ]
    SELECTED_STATE_COMMANDS = AUTHENTICATED_STATE_COMMANDS + [
      "CHECK",
      "CLOSE",
      "EXPUNGE",
      "UID SEARCH",
      "FETCH",
      "UID FETCH",
      "UID STORE",
      "UID COPY"
    ]
    LOGOUT_STATE_COMMANDS = []
    COMMANDS = {
      NON_AUTHENTICATED_STATE => NON_AUTHENTICATED_STATE_COMMANDS,
      AUTHENTICATED_STATE => AUTHENTICATED_STATE_COMMANDS,
      SELECTED_STATE => SELECTED_STATE_COMMANDS,
      LOGOUT_STATE => LOGOUT_STATE_COMMANDS
    }

    def command
      result = NullCommand.new
      token = lookahead
      if token.symbol == T_CRLF || token.symbol == T_EOF
        result = NullCommand.new
      else
        tag = atom
        token = lookahead
        if token.symbol == T_CRLF || token.symbol == T_EOF
          result = MissingCommand.new
        else
          match(T_SPACE)
          name = atom.upcase
          if name == "UID"
            match(T_SPACE)
            name += " " + atom.upcase
          end
          if COMMANDS[@session.state].include?(name)
            result = send(name.tr(" ", "_").downcase)
            result.name = name
            match(T_CRLF)
            match(T_EOF)
          else
            result = UnrecognizedCommand.new
          end
        end
        result.tag = tag
      end
      result.session = @session
      return result
    end

    def capability
      return CapabilityCommand.new
    end

    def noop
      return NoopCommand.new
    end

    def logout
      return LogoutCommand.new
    end

    def authenticate
      match(T_SPACE)
      auth_type = atom.upcase
      case auth_type
      when "CRAM-MD5"
        return AuthenticateCramMD5Command.new
      else
        raise format("unknown auth type: %s", auth_type)
      end
    end

    def login
      match(T_SPACE)
      userid = astring
      match(T_SPACE)
      password = astring
      return LoginCommand.new(userid, password)
    end

    def select
      match(T_SPACE)
      mailbox_name = mailbox
      return SelectCommand.new(mailbox_name)
    end

    def examine
      match(T_SPACE)
      mailbox_name = mailbox
      return ExamineCommand.new(mailbox_name)
    end

    def create
      match(T_SPACE)
      mailbox_name = mailbox
      return CreateCommand.new(mailbox_name)
    end

    def delete
      match(T_SPACE)
      mailbox_name = mailbox
      return DeleteCommand.new(mailbox_name)
    end

    def rename
      match(T_SPACE)
      mailbox_name = mailbox
      match(T_SPACE)
      new_mailbox_name = mailbox
      return RenameCommand.new(mailbox_name, new_mailbox_name)
    end

    def subscribe
      match(T_SPACE)
      mailbox_name = mailbox
      return NoopCommand.new
    end

    def unsubscribe
      match(T_SPACE)
      mailbox_name = mailbox
      return NoopCommand.new
    end

    def list
      match(T_SPACE)
      reference_name = mailbox
      match(T_SPACE)
      mailbox_name = list_mailbox
      return ListCommand.new(reference_name, mailbox_name)
    end

    def lsub
      match(T_SPACE)
      reference_name = mailbox
      match(T_SPACE)
      mailbox_name = list_mailbox
      return ListCommand.new(reference_name, mailbox_name)
    end

    def list_mailbox
      token = lookahead
      if string_token?(token)
        s = string
        if /\AINBOX\z/ni.match(s)
          return "INBOX"
        else
          return s
        end
      else
        result = ""
        loop do
          token = lookahead
          if list_mailbox_token?(token)
            result.concat(token.value)
            shift_token
          else
            if result.empty?
              parse_error("unexpected token %s", token.symbol)
            else
              if /\AINBOX\z/ni.match(result)
                return "INBOX"
              else
                return result
              end
            end
          end
        end
      end
    end

    LIST_MAILBOX_TOKENS = [
      T_ATOM,
      T_NUMBER,
      T_NIL,
      T_LBRA,
      T_RBRA,
      T_PLUS,
      T_STAR,
      T_PERCENT
    ]

    def list_mailbox_token?(token)
      return LIST_MAILBOX_TOKENS.include?(token.symbol)
    end

    def status
      match(T_SPACE)
      mailbox_name = mailbox
      match(T_SPACE)
      match(T_LPAR)
      atts = []
      atts.push(status_att)
      loop do
        token = lookahead
        if token.symbol == T_RPAR
          shift_token
          break
        end
        match(T_SPACE)
        atts.push(status_att)
      end
      return StatusCommand.new(mailbox_name, atts)
    end

    def status_att
      att = atom.upcase
      unless /\A(MESSAGES|RECENT|UIDNEXT|UIDVALIDITY|UNSEEN)\z/.match(att)
        parse_error("unknown att `%s'", att)
      end
      return att
    end

    def mailbox
      result = astring
      if /\AINBOX\z/ni.match(result)
        return "INBOX"
      else
        return result
      end
    end

    def append
      match(T_SPACE)
      mailbox_name = mailbox
      match(T_SPACE)
      token = lookahead
      if token.symbol == T_LPAR
        flags = flag_list
        match(T_SPACE)
        token = lookahead
      else
        flags = []
      end
      if token.symbol == T_QUOTED
        shift_token
        datetime = token.value
        match(T_SPACE)
      else
        datetime = nil
      end
      token = match(T_LITERAL)
      message = token.value
      return AppendCommand.new(mailbox_name, flags, datetime, message)
    end

    def idle
      return IdleCommand.new
    end

    def check
      return NoopCommand.new
    end

    def close
      return CloseCommand.new
    end

    def expunge
      return ExpungeCommand.new
    end

    def uid_search
      match(T_SPACE)
      token = lookahead
      if token.value == "CHARSET"
        shift_token
        match(T_SPACE)
        charset = astring
        match(T_SPACE)
      else
        charset = "us-ascii"
      end
      return UidSearchCommand.new(search_keys(charset))
    end

    def search_keys(charset)
      result = [search_key(charset)]
      loop do
        token = lookahead
        if token.symbol != T_SPACE
          break
        end
        shift_token
        result.push(search_key(charset))
      end
      return result
    end

    def search_key(charset)
      name = tokens([T_ATOM, T_NUMBER, T_NIL, T_PLUS, T_STAR])
      case name
      when "BODY"
        match(T_SPACE)
        s = Iconv.conv("utf-8", charset, astring)
        return BodySearchKey.new(s)
      when "HEADER"
        match(T_SPACE)
        header_name = astring.downcase
        match(T_SPACE)
        s = Iconv.conv("utf-8", charset, astring)
        return HeaderSearchKey.new(header_name, s)
      when "ANSWERED"
        return FlagSearchKey.new(@session.mail_store, "\\Answered")
      when "DELETED"
        return FlagSearchKey.new(@session.mail_store, "\\Deleted")
      when "DRAFT"
        return FlagSearchKey.new(@session.mail_store, "\\Draft")
      when "FLAGGED"
        return FlagSearchKey.new(@session.mail_store, "\\Flagged")
      when "RECENT", "NEW"
        return FlagSearchKey.new(@session.mail_store, "\\Recent")
      when "SEEN"
        return FlagSearchKey.new(@session.mail_store, "\\Seen")
      when "UNANSWERED"
        return NoFlagSearchKey.new(@session.mail_store, "\\Answered")
      when "UNDELETED"
        return NoFlagSearchKey.new(@session.mail_store, "\\Deleted")
      when "UNDRAFT"
        return NoFlagSearchKey.new(@session.mail_store, "\\Draft")
      when "UNFLAGGED"
        return NoFlagSearchKey.new(@session.mail_store, "\\Flagged")
      when "UNSEEN"
        return NoFlagSearchKey.new(@session.mail_store, "\\Seen")
      when "OLD"
        return NoFlagSearchKey.new(@session.mail_store, "\\Recent")
      when /\A(\d+|\*)\z/
        return UidSearchKey.new(parse_seq_number($1))
      when /\A(\d+|\*):(\d+|\*)\z/
        return UidRangeSearchKey.new(parse_seq_number($1),
                                     parse_seq_number($2))
      when "NOT"
        match(T_SPACE)
        return NotSearchKey.new(search_key(charset))
      else
        return NullSearchKey.new
      end
    end

    def fetch
      match(T_SPACE)
      seq_set = sequence_set
      match(T_SPACE)
      atts = fetch_atts
      return FetchCommand.new(seq_set, atts)
    end

    def uid_fetch
      match(T_SPACE)
      seq_set = sequence_set
      match(T_SPACE)
      atts = fetch_atts
      return UidFetchCommand.new(seq_set, atts)
    end

    def fetch_atts
      token = lookahead
      if token.symbol == T_LPAR
        shift_token
        result = []
        result.push(fetch_att)
        loop do
          token = lookahead
          if token.symbol == T_RPAR
            shift_token
            break
          end
          match(T_SPACE)
          result.push(fetch_att)
        end
        return result
      else
        case token.value
        when "ALL"
          shift_token
          result = []
          result.push(FlagsFetchAtt.new)
          result.push(InternalDateFetchAtt.new)
          result.push(RFC822SizeFetchAtt.new)
          result.push(EnvelopeFetchAtt.new)
          return result
        when "FAST"
          shift_token
          result = []
          result.push(FlagsFetchAtt.new)
          result.push(InternalDateFetchAtt.new)
          result.push(RFC822SizeFetchAtt.new)
          return result
        when "FULL"
          shift_token
          result = []
          result.push(FlagsFetchAtt.new)
          result.push(InternalDateFetchAtt.new)
          result.push(RFC822SizeFetchAtt.new)
          result.push(EnvelopeFetchAtt.new)
          result.push(BodyFetchAtt.new)
          return result
        else
          return [fetch_att]
        end
      end
    end

    def fetch_att
      token = match(T_ATOM)
      case token.value
      when /\A(?:ENVELOPE)\z/ni
        return EnvelopeFetchAtt.new
      when /\A(?:FLAGS)\z/ni
        return FlagsFetchAtt.new
      when /\A(?:RFC822)\z/ni
        return RFC822FetchAtt.new
      when /\A(?:RFC822\.HEADER)\z/ni
        return RFC822HeaderFetchAtt.new
      when /\A(?:RFC822\.SIZE)\z/ni
        return RFC822SizeFetchAtt.new
      when /\A(?:BODY)?\z/ni
        token = lookahead
        if token.symbol != T_LBRA
          return BodyFetchAtt.new
        end
        return BodySectionFetchAtt.new(section, opt_partial, false)
      when /\A(?:BODY\.PEEK)\z/ni
        return BodySectionFetchAtt.new(section, opt_partial, true)
      when /\A(?:BODYSTRUCTURE)\z/ni
        return BodyStructureFetchAtt.new
      when /\A(?:UID)\z/ni
        return UidFetchAtt.new
      when /\A(?:INTERNALDATE)\z/ni
        return InternalDateFetchAtt.new
      else
        parse_error("unknown attribute `%s'", token.value)
      end
    end

    def section
      match(T_LBRA)
      token = lookahead
      if token.symbol != T_RBRA
        s = tokens([T_ATOM, T_NUMBER, T_NIL, T_PLUS])
        case s
        when /\A(?:(?:([0-9.]+)\.)?(HEADER|TEXT))\z/ni
          result = Section.new($1, $2.upcase)
        when /\A(?:(?:([0-9.]+)\.)?(HEADER\.FIELDS(?:\.NOT)?))\z/ni
          match(T_SPACE)
          result = Section.new($1, $2.upcase, header_list)
        when /\A(?:([0-9.]+)\.(MIME))\z/ni
          result = Section.new($1, $2.upcase)
        when /\A([0-9.]+)\z/ni
          result = Section.new($1)
        else
          parse_error("unknown section `%s'", s)
        end
      end
      match(T_RBRA)
      return result
    end

    def header_list
      result = []
      match(T_LPAR)
      result.push(astring.upcase)
      loop do
        token = lookahead
        if token.symbol == T_RPAR
          shift_token
          break
        end
        match(T_SPACE)
        result.push(astring.upcase)
      end
      return result
    end

    def opt_partial
      token = lookahead
      if m = /<(\d+)\.(\d+)>/.match(token.value)
        shift_token
        return Partial.new(m[1].to_i, m[2].to_i)
      end
      return nil
    end
    Partial = Struct.new(:offset, :size)

    def uid_store
      match(T_SPACE)
      seq_set = sequence_set
      match(T_SPACE)
      att = store_att_flags
      return UidStoreCommand.new(seq_set, att)
    end

    def store_att_flags
      item = atom
      match(T_SPACE)
      token = lookahead
      if token.symbol == T_LPAR
        flags = flag_list
      else
        flags = []
        flags.push(flag)
        loop do
          token = lookahead
          if token.symbol != T_SPACE
            break
          end
          shift_token
          flags.push(flag)
        end
      end
      case item
      when /\AFLAGS(\.SILENT)?\z/ni
        return SetFlagsStoreAtt.new(flags, !$1.nil?)
      when /\A\+FLAGS(\.SILENT)?\z/ni
        return AddFlagsStoreAtt.new(flags, !$1.nil?)
      when /\A-FLAGS(\.SILENT)?\z/ni
        return RemoveFlagsStoreAtt.new(flags, !$1.nil?)
      else
        parse_error("unkown data item - `%s'", item)
      end
    end

    FLAG_REGEXP = /\
(?# FLAG        )(\\[^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
(?# ATOM        )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n

    def flag_list
      match(T_LPAR)
      if @str.index(/([^)]*)\)/ni, @pos)
        @pos = $~.end(0)
        return $1.scan(FLAG_REGEXP).collect { |flag, atom|
          atom || flag
        }
      else
        parse_error("invalid flag list")
      end
    end

    EXACT_FLAG_REGEXP = /\A\
(?# FLAG        )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\
(?# ATOM        )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)\z/n

    def flag
      result = atom
      unless EXACT_FLAG_REGEXP.match(s)
        parse_error("invalid flag")
      end
      return result
    end

    def sequence_set
      s = ""
      loop do
        token = lookahead
        break if !atom_token?(token) && token.symbol != T_STAR
        shift_token
        s.concat(token.value)
      end
      return s.split(/,/n).collect { |i|
        x, y = i.split(/:/n)
        if y.nil?
          parse_seq_number(x)
        else
          parse_seq_number(x) .. parse_seq_number(y)
        end
      }
    end

    def parse_seq_number(s)
      if s == "*"
        return -1
      else
        return s.to_i
      end
    end

    def uid_copy
      match(T_SPACE)
      seq_set = sequence_set
      match(T_SPACE)
      mailbox_name = mailbox
      return UidCopyCommand.new(seq_set, mailbox_name)
    end

    def astring
      token = lookahead
      if string_token?(token)
        return string
      else
        return atom
      end
    end

    def string
      token = lookahead
      if token.symbol == T_NIL
        shift_token
        return nil
      end
      token = match(T_QUOTED, T_LITERAL)
      return token.value
    end

    STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL]

    def string_token?(token)
      return STRING_TOKENS.include?(token.symbol)
    end

    def case_insensitive_string
      token = lookahead
      if token.symbol == T_NIL
        shift_token
        return nil
      end
      token = match(T_QUOTED, T_LITERAL)
      return token.value.upcase
    end

    def tokens(tokens)
      result = ""
      loop do
        token = lookahead
        if tokens.include?(token.symbol)
          result.concat(token.value)
          shift_token
        else
          if result.empty?
            parse_error("unexpected token %s", token.symbol)
          else
            return result
          end
        end
      end
    end

    ATOM_TOKENS = [
      T_ATOM,
      T_NUMBER,
      T_NIL,
      T_LBRA,
      T_RBRA,
      T_PLUS
    ]

    def atom
      return tokens(ATOM_TOKENS)
    end

    def atom_token?(token)
      return ATOM_TOKENS.include?(token.symbol)
    end

    def number
      token = lookahead
      if token.symbol == T_NIL
        shift_token
        return nil
      end
      token = match(T_NUMBER)
      return token.value.to_i
    end

    def nil_atom
      match(T_NIL)
      return nil
    end

    def match(*args)
      token = lookahead
      unless args.include?(token.symbol)
        parse_error('unexpected token %s (expected %s)',
                    token.symbol.id2name,
                    args.collect {|i| i.id2name}.join(" or "))
      end
      shift_token
      return token
    end

    def lookahead
      unless @token
        @token = next_token
      end
      return @token
    end

    def shift_token
      @token = nil
    end

    def next_token
      case @lex_state
      when EXPR_BEG
        if @str.index(BEG_REGEXP, @pos)
          @pos = $~.end(0)
          if $1
            return Token.new(T_SPACE, $+)
          elsif $2
            return Token.new(T_NIL, $+)
          elsif $3
            return Token.new(T_NUMBER, $+)
          elsif $4
            return Token.new(T_ATOM, $+)
          elsif $5
            return Token.new(T_QUOTED,
                             $+.gsub(/\\(["\\])/n, "\\1"))
          elsif $6
            return Token.new(T_LPAR, $+)
          elsif $7
            return Token.new(T_RPAR, $+)
          elsif $8
            return Token.new(T_BSLASH, $+)
          elsif $9
            return Token.new(T_STAR, $+)
          elsif $10
            return Token.new(T_LBRA, $+)
          elsif $11
            return Token.new(T_RBRA, $+)
          elsif $12
            len = $+.to_i
            val = @str[@pos, len]
            @pos += len
            return Token.new(T_LITERAL, val)
          elsif $13
            return Token.new(T_PLUS, $+)
          elsif $14
            return Token.new(T_PERCENT, $+)
          elsif $15
            return Token.new(T_CRLF, $+)
          elsif $16
            return Token.new(T_EOF, $+)
          else
            parse_error("[ximapd BUG] BEG_REGEXP is invalid")
          end
        else
          @str.index(/\S*/n, @pos)
          parse_error("unknown token - %s", $&.dump)
        end
      when EXPR_DATA
        if @str.index(DATA_REGEXP, @pos)
          @pos = $~.end(0)
          if $1
            return Token.new(T_SPACE, $+)
          elsif $2
            return Token.new(T_NIL, $+)
          elsif $3
            return Token.new(T_NUMBER, $+)
          elsif $4
            return Token.new(T_QUOTED,
                             $+.gsub(/\\(["\\])/n, "\\1"))
          elsif $5
            len = $+.to_i
            val = @str[@pos, len]
            @pos += len
            return Token.new(T_LITERAL, val)
          elsif $6
            return Token.new(T_LPAR, $+)
          elsif $7
            return Token.new(T_RPAR, $+)
          else
            parse_error("[ximapd BUG] BEG_REGEXP is invalid")
          end
        else
          @str.index(/\S*/n, @pos)
          parse_error("unknown token - %s", $&.dump)
        end
      when EXPR_TEXT
        if @str.index(TEXT_REGEXP, @pos)
          @pos = $~.end(0)
          if $1
            return Token.new(T_TEXT, $+)
          else
            parse_error("[ximapd BUG] TEXT_REGEXP is invalid")
          end
        else
          @str.index(/\S*/n, @pos)
          parse_error("unknown token - %s", $&.dump)
        end
      when EXPR_RTEXT
        if @str.index(RTEXT_REGEXP, @pos)
          @pos = $~.end(0)
          if $1
            return Token.new(T_LBRA, $+)
          elsif $2
            return Token.new(T_TEXT, $+)
          else
            parse_error("[ximapd BUG] RTEXT_REGEXP is invalid")
          end
        else
          @str.index(/\S*/n, @pos)
          parse_error("unknown token - %s", $&.dump)
        end
      when EXPR_CTEXT
        if @str.index(CTEXT_REGEXP, @pos)
          @pos = $~.end(0)
          if $1
            return Token.new(T_TEXT, $+)
          else
            parse_error("[ximapd BUG] CTEXT_REGEXP is invalid")
          end
        else
          @str.index(/\S*/n, @pos) #/
          parse_error("unknown token - %s", $&.dump)
        end
      else
        parse_error("illegal @lex_state - %s", @lex_state.inspect)
      end
    end

    def parse_error(fmt, *args)
      @logger.debug("@str: #{@str.inspect}")
      @logger.debug("@pos: #{@pos}")
      @logger.debug("@lex_state: #{@lex_state}")
      if @token && @token.symbol
        @logger.debug("@token.symbol: #{@token.symbol}")
        @logger.debug("@token.value: #{@token.value.inspect}")
      end
      raise CommandParseError, format(fmt, *args)
    end
  end

  class CommandParseError < StandardError
  end

  class Command
    attr_reader :session
    attr_accessor :tag, :name

    def initialize
      @session = nil
      @config = nil
      @tag = nil
      @name = nil
    end

    def session=(session)
      @session = session
      @config = session.config
      @mail_store = session.mail_store
      @logger = @config["logger"]
    end

    def send_tagged_ok(code = nil)
      if code.nil?
        @session.send_tagged_ok(@tag, "%s completed", @name)
      else
        @session.send_tagged_ok(@tag, "[%s] %s completed", code, @name)
      end
    end
  end

  class NullCommand < Command
    def exec
      @session.send_bad("Null command")
    end
  end

  class MissingCommand < Command
    def exec
      @session.send_tagged_bad(@tag, "Missing command")
    end
  end

  class UnrecognizedCommand < Command
    def exec
      msg = "Command unrecognized"
      if @session.state == NON_AUTHENTICATED_STATE
        msg.concat("/login please")
      end
      @session.send_tagged_bad(@tag, msg)
    end
  end

  class CapabilityCommand < Command
    def exec
      @session.send_data("CAPABILITY IMAP4REV1 IDLE LOGINDISABLED AUTH=CRAM-MD5")
      send_tagged_ok
    end
  end

  class NoopCommand < Command
    def exec
      @session.synchronize do
        @session.sync
      end
      send_tagged_ok
    end
  end

  class LogoutCommand < Command
    def exec
      @session.synchronize do
        @session.sync
      end
      @session.send_data("BYE IMAP server terminating connection")
      send_tagged_ok
      @session.logout
    end
  end

  class AuthenticateCramMD5Command < Command
    def exec
      challenge = @@challenge_generator.call
      @session.send_continue_req([challenge].pack("m").gsub("\n", ""))
      line = @session.recv_line
      s = line.unpack("m")[0]
      digest = hmac_md5(challenge, @config["password"])
      expected = @config["user"] + " " + digest
      if s == expected
        @session.login
        send_tagged_ok
      else
        sleep(3)
        @session.send_tagged_no(@tag, "AUTHENTICATE failed")
      end
    end

    @@challenge_generator = Proc.new {
      format("<%s.%f@%s>",
             Process.pid, Time.new.to_f,
             TCPSocket.gethostbyname(Socket.gethostname)[0])
    }

    def self.challenge_generator
      return @@challenge_generator
    end

    def self.challenge_generator=(proc)
      @@challenge_generator = proc
    end

    private

    def hmac_md5(text, key)
      if key.length > 64
        key = Digest::MD5.digest(key)
      end

      k_ipad = key + "\0" * (64 - key.length)
      k_opad = key + "\0" * (64 - key.length)
      for i in 0..63
        k_ipad[i] ^= 0x36
        k_opad[i] ^= 0x5c
      end

      digest = Digest::MD5.digest(k_ipad + text)

      return Digest::MD5.hexdigest(k_opad + digest)
    end
  end

  class LoginCommand < Command
    def initialize(userid, password)
      @userid = userid
      @password = password
    end

    def exec
      @session.send_tagged_no(@tag, "LOGIN failed")
    end
  end

  class MailboxCheckCommand < Command
    def initialize(mailbox_name)
      @mailbox_name = mailbox_name
    end

    def exec
      begin
        status = nil
        @session.synchronize do
          status = @mail_store.get_mailbox_status(@mailbox_name,
                                                  @session.read_only?)
        end
        @session.send_data("%d EXISTS", status.messages)
        @session.send_data("%d RECENT", status.recent)
        @session.send_ok("[UIDVALIDITY %d] UIDs valid", status.uidvalidity)
        @session.send_ok("[UIDNEXT %d] Predicted next UID", status.uidnext)
        @session.send_data("FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)")
        @session.send_ok("[PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited")
        send_tagged_response
      rescue MailboxError => e
        @session.send_tagged_no(@tag, "%s", e)
      end
    end

    private

    def send_tagged_response
      raise ScriptError.new("subclass must override send_tagged_response")
    end
  end

  class SelectCommand < MailboxCheckCommand
    private

    def send_tagged_response
      @session.synchronize do
        #@session.sync
        @session.select(@mailbox_name)
      end
      send_tagged_ok("READ-WRITE")
    end
  end

  class ExamineCommand < MailboxCheckCommand
    private

    def send_tagged_response
      @session.synchronize do
        #@session.sync
        @session.examine(@mailbox_name)
      end
      send_tagged_ok("READ-ONLY")
    end
  end

  class CreateCommand < Command
    def initialize(mailbox_name)
      @mailbox_name = mailbox_name
    end

    def exec
      begin
        @session.synchronize do
          @mail_store.create_mailbox(@mailbox_name)
        end
        send_tagged_ok
      rescue InvalidQueryError
        @session.send_tagged_no(@tag, "invalid query")
      end
    end
  end

  class DeleteCommand < Command
    def initialize(mailbox_name)
      @mailbox_name = mailbox_name
    end

    def exec
      if /\A(INBOX|ml|queries)\z/ni.match(@mailbox_name)
        @session.send_tagged_no(@tag, "can't delete %s", @mailbox_name)
        return
      end
      @session.synchronize do
        @mail_store.delete_mailbox(@mailbox_name)
      end
      send_tagged_ok
    end
  end

  class RenameCommand < Command
    def initialize(mailbox_name, new_mailbox_name)
      @mailbox_name = mailbox_name
      @new_mailbox_name = new_mailbox_name
    end

    def exec
      if /\A(INBOX|ml|queries)\z/ni.match(@mailbox_name)
        @session.send_tagged_no(@tag, "can't rename %s", @mailbox_name)
        return
      end
      begin
        @session.synchronize do
          @mail_store.rename_mailbox(@mailbox_name, @new_mailbox_name)
        end
        send_tagged_ok
      rescue InvalidQueryError
        @session.send_tagged_no(@tag, "invalid query")
      end
    end
  end

  class ListCommand < Command
    include DataFormat

    def initialize(reference_name, mailbox_name)
      @reference_name = reference_name
      @mailbox_name = mailbox_name
    end

    def exec
      unless @reference_name.empty?
        @session.send_tagged_no(@tag, "%s failed", @name)
        return
      end
      if @mailbox_name.empty?
        @session.send_data("%s (\\Noselect) \"/\" \"\"", @name)
        send_tagged_ok
        return
      end
      pat = @mailbox_name.gsub(/\*|%|[^*%]+/n) { |s|
        case s
        when "*"
          ".*"
        when "%"
          "[^/]*"
        else
          Regexp.quote(s)
        end
      }
      re = Regexp.new("\\A" + pat + "\\z", nil, "n")
      mailboxes = nil
      @session.synchronize do
        mailboxes = @mail_store.mailboxes.to_a.select { |mbox_name,|
          re.match(mbox_name)
        }
      end
      mailboxes.sort_by { |i| i[0] }.each do |mbox_name, mbox|
        @session.send_data("%s (%s) \"/\" %s",
                           @name, mbox["flags"], quoted(mbox_name))
      end
      send_tagged_ok
    end
  end

  class StatusCommand < Command
    include DataFormat

    def initialize(mailbox_name, atts)
      @mailbox_name = mailbox_name
      @atts = atts
    end

    def exec
      status = nil
      @session.synchronize do
        status = @mail_store.get_mailbox_status(@mailbox_name,
                                                @session.read_only?)
      end
      s = @atts.collect { |att|
        format("%s %d", att, status.send(att.downcase))
      }.join(" ")
      @session.send_data("STATUS %s (%s)", quoted(@mailbox_name), s)
      send_tagged_ok
    end
  end

  class AppendCommand < Command
    def initialize(mailbox_name, flags, datetime, message)
      @mailbox_name = mailbox_name
      @flags = flags
      @datetime = datetime
      @message = message
    end

    def exec
      @session.synchronize do
        @mail_store.import_mail(@message, @mailbox_name, @flags.join(" "))
      end
      send_tagged_ok
    end
  end

  class IdleCommand < Command
    def exec
      @session.send_continue_req("Waiting for DONE")
      @session.synchronize do
        @session.sync
        @mail_store.mailbox_db.transaction do
          for plugin in @mail_store.plugins
            plugin.on_idle
          end
        end
      end
      @session.recv_line
      send_tagged_ok
    end
  end

  class CloseCommand < Command
    def exec
      @session.synchronize do
        @session.sync
        mailbox = @session.get_current_mailbox
        mails = mailbox.fetch([1..-1])
        deleted_mails = mails.select { |mail|
          /\\Deleted\b/ni.match(mail.flags(false))
        }
        @mail_store.delete_mails(deleted_mails)
      end
      @session.close_mailbox
      send_tagged_ok
    end
  end

  class ExpungeCommand < Command
    def exec
      deleted_seqnos = nil
      @session.synchronize do
        @session.sync
        mailbox = @session.get_current_mailbox
        mails = mailbox.fetch([1..-1])
        deleted_mails = mails.select { |mail|
          /\\Deleted\b/ni.match(mail.flags(false))
        }.reverse
        deleted_seqnos = deleted_mails.collect { |mail|
          mail.seqno
        }
        @mail_store.delete_mails(deleted_mails)
      end
      for seqno in deleted_seqnos
        @session.send_data("%d EXPUNGE", seqno)
      end
      send_tagged_ok
    end
  end

  class UidSearchCommand < Command
    def initialize(keys)
      @keys = keys
    end

    def exec
      query = @keys.collect { |key| key.to_query }.reject { |q|
        q.empty?
      }.join(" ")
      uids = nil
      @session.synchronize do
        mailbox = @session.get_current_mailbox
        uids = mailbox.uid_search(query)
        for key in @keys
          uids = key.select(uids)
        end
      end
      if uids.empty?
        @session.send_data("SEARCH")
      else
        @session.send_data("SEARCH %s", uids.join(" "))
      end
      send_tagged_ok
    end
  end

  class NullSearchKey
    def to_query
      return ""
    end

    def select(uids)
      return uids
    end
  end

  class BodySearchKey < NullSearchKey
    def initialize(value)
      @value = value
    end

    def to_query
      return @value
    end
  end

  class HeaderSearchKey < NullSearchKey
    include QueryFormat

    def initialize(name, value)
      @name = name
      @value = value
    end

    def to_query
      case @name
      when "subject", "from", "to", "cc"
        return format("%s : %s", @name, quote_query(@value))
      when "x-ml-name", "x-mail-count"
        return format("%s = %s", @name, quote_query(@value))
      else
        return ""
      end
    end
  end

  class FlagSearchKey < NullSearchKey
    def initialize(mail_store, flag)
      @mail_store = mail_store
      @flag_re = Regexp.new(Regexp.quote(flag) + "\\b", true, "n")
    end

    def select(uids)
      return uids.select { |uid|
        @flag_re.match(@mail_store.flags_db[uid.to_s])
      }
    end
  end

  class NoFlagSearchKey < FlagSearchKey
    def select(uids)
      return uids.select { |uid|
        !@flag_re.match(@mail_store.flags_db[uid.to_s])
      }
    end
  end

  class UidSearchKey < NullSearchKey
    def initialize(uid)
      @uid = uid
    end

    def to_query
      if @uid == -1
        return ""
      else
        return format("uid = %d", @uid)
      end
    end

    def select(uids)
      if @uid == -1
        return [uids.sort.last]
      else
        return uids
      end
    end
  end

  class UidRangeSearchKey < NullSearchKey
    def initialize(first, last)
      @first = first
      @last = last
    end

    def to_query
      if @last == -1
        return format("uid >= %d", @first)
      else
        return format("%d <= uid <= %d", @first, @last)
      end
    end
  end

  class NotSearchKey
    def initialize(key)
      @key = key
    end

    def to_query
      q = @key.to_query
      if q.empty?
        return ""
      else
        return format("! ( %s )", q)
      end
    end

    def select(uids)
      rejected = @key.select(uids)
      return uids - rejected
    end
  end

  class FetchCommand < Command
    def initialize(sequence_set, atts)
      @sequence_set = sequence_set
      @atts = atts
    end

    def exec
      mails = nil
      @session.synchronize do
        mailbox = @session.get_current_mailbox
        mails = mailbox.fetch(@sequence_set)
      end
      for mail in mails
        data = nil
        @session.synchronize do
          data = @atts.collect { |att|
            att.fetch(mail)
          }.join(" ")
        end
        @session.send_data("%d FETCH (%s)", mail.seqno, data)
      end
      send_tagged_ok
    end
  end

  class UidFetchCommand < FetchCommand
    def initialize(sequence_set, atts)
      super(sequence_set, atts)
      unless @atts[0].kind_of?(UidFetchAtt)
        @atts.unshift(UidFetchAtt.new)
      end
    end

    def exec
      mails = nil
      @session.synchronize do
        mailbox = @session.get_current_mailbox
        mails = mailbox.uid_fetch(@sequence_set)
      end
      for mail in mails
        data = nil
        @session.synchronize do
          data = @atts.collect { |att|
            att.fetch(mail)
          }.join(" ")
        end
        @session.send_data("%d FETCH (%s)", mail.uid, data)
      end
      send_tagged_ok
    end
  end

  class EnvelopeFetchAtt
    def fetch(mail)
      return format("ENVELOPE %s", mail.envelope)
    end
  end

  class FlagsFetchAtt
    def fetch(mail)
      return format("FLAGS (%s)", mail.flags)
    end
  end

  class InternalDateFetchAtt
    include DataFormat

    def fetch(mail)
      indate = mail.internal_date.strftime("%d-%b-%Y %H:%M:%S %z")
      return format("INTERNALDATE %s", quoted(indate))
    end
  end

  class RFC822FetchAtt
    include DataFormat

    def fetch(mail)
      return format("RFC822 %s", literal(mail.to_s))
    end
  end

  class RFC822HeaderFetchAtt
    include DataFormat

    def fetch(mail)
      return format("RFC822.HEADER %s", literal(mail.header))
    end
  end

  class RFC822SizeFetchAtt
    def fetch(mail)
      return format("RFC822.SIZE %s", mail.size)
    end
  end

  class RFC822TextFetchAtt
    def fetch(mail)
    end
  end

  class BodyFetchAtt
    def fetch(mail)
      return format("BODY %s", mail.body)
    end
  end

  class BodyStructureFetchAtt
    def fetch(mail)
      return format("BODYSTRUCTURE %s", mail.body)
    end
  end

  class UidFetchAtt
    def fetch(mail)
      return format("UID %s", mail.uid)
    end
  end

  class BodySectionFetchAtt
    include DataFormat

    def initialize(section, partial, peek)
      @section = section
      @partial = partial
      @peek = peek
    end

    def fetch(mail)
      if @section.nil? ||
        (@section.text.nil? &&
         (@section.part.nil? || @section.part == "1") &&
         !mail.multipart?)
        if @section.nil?
          part = ""
        else
          part = @section.part
        end
        if @partial.nil?
          result = format("BODY[%s] %s", part, literal(mail.to_s))
        else
          s = mail.to_s[@partial.offset, @partial.size] 
          result = format("BODY[%s]<%d> %s",
                          part, @partial.offset, literal(s))
        end
        unless @peek
          flags = mail.flags(false)
          unless /\\Seen\b/ni.match(flags)
            if flags.empty?
              flags = "\\Seen"
            else
              flags += " \\Seen"
            end
            mail.flags = flags
            result += format(" FLAGS (%s)", flags)
          end
        end
        return result
      end
      case @section.text
      when "HEADER"
        return format("BODY[HEADER] %s", literal(mail.header))
      when "HEADER.FIELDS"
        s = mail.header_fields(@section.header_list)
        fields = @section.header_list.collect { |i|
          quoted(i)
        }.join(" ")
        return format("BODY[HEADER.FIELDS (%s)] %s",
                      fields, literal(s))
      end
    end
  end

  Section = Struct.new(:part, :text, :header_list)

  class UidStoreCommand < Command
    def initialize(sequence_set, att)
      @sequence_set = sequence_set
      @att = att
    end

    def exec
      mails = nil
      @session.synchronize do
        mailbox = @session.get_current_mailbox
        mails = mailbox.uid_fetch(@sequence_set)
      end
      unless @session.read_only?
        for mail in mails
          flags = nil
          @session.synchronize do
            @att.store(mail)
            flags = mail.flags
          end
          unless @att.silent?
            @session.send_data("%d FETCH (FLAGS (%s) UID %d)",
                               mail.uid, flags, mail.uid)
          end
        end
      end
      send_tagged_ok
    end
  end

  class FlagsStoreAtt
    def initialize(flags, silent = false)
      @flags = flags
      @silent = silent
    end

    def silent?
      return @silent
    end
  end

  class SetFlagsStoreAtt < FlagsStoreAtt
    def store(mail)
      mail.flags = @flags.join(" ")
    end
  end

  class AddFlagsStoreAtt < FlagsStoreAtt
    def store(mail)
      flags = mail.flags(false).split(/ /)
      flags |= @flags
      mail.flags = flags.join(" ")
    end
  end

  class RemoveFlagsStoreAtt < FlagsStoreAtt
    def store(mail)
      flags = mail.flags(false).split(/ /)
      flags -= @flags
      mail.flags = flags.join(" ")
    end
  end

  class UidCopyCommand < Command
    def initialize(sequence_set, mailbox_name)
      @sequence_set = sequence_set
      @mailbox_name = mailbox_name
    end

    def exec
      mails = nil
      @session.synchronize do
        mailbox = @session.get_current_mailbox
        mails = mailbox.uid_fetch(@sequence_set)
      end
      for mail in mails
        @session.synchronize do
          @mail_store.import_mail(mail.to_s, @mailbox_name)
          for plugin in @mail_store.plugins
            plugin.on_copy(mail, @mailbox_name)
          end
        end
      end
      send_tagged_ok
    end
  end

  class TerminateException < Exception; end
  class MailboxError < StandardError; end
  class MailboxExistError < MailboxError; end
  class NoMailboxError < MailboxError; end
  class MailboxAccessError < MailboxError; end
  class NotSelectableMailboxError < MailboxError; end
  class InvalidQueryError < StandardError; end

  class Plugin
    @@directories = nil
    @@loaded = false

    def self.directories=(dirs)
      @@directories = dirs
    end

    def self.create_plugins(config, mail_store)
      return [] unless config.key?("plugins")
      if !@@loaded && @@directories
        logger = config["logger"]
        for plugin in config["plugins"]
          basename = plugin["name"].downcase + ".rb"
          filename = @@directories.collect { |dir|
            File.expand_path(basename, dir)
          }.detect { |filename| File.exist?(filename) }
          raise "#{basename} not found" unless filename
          File.open(filename) do |f|
            Ximapd.class_eval(f.read)
          end
          logger.debug("loaded plugin: #{filename}")
        end
        @@loaded = true
      end
      return config["plugins"].collect { |plugin|
        Ximapd.const_get(plugin["name"]).new(plugin, mail_store,
                                             config["logger"])
      }
    end

    def initialize(config, mail_store, logger)
      @config = config
      @mail_store = mail_store
      @logger = logger
      init_plugin
    end

    def init_plugin
    end

    def filter(mail)
      return nil
    end

    def on_copy(mail, mailbox_name)
    end

    def on_delete_mail(mail)
    end

    def on_idle
    end
  end

  class ConsoleSocket
    [:read, :gets, :getc].each do |mid|
      define_method(mid) do |*args|
        STDIN.send(mid, *args)
      end
    end

    [:write, :print].each do |mid|
      define_method(mid) do |*args|
        STDOUT.send(mid, *args)
      end
    end

    def peeraddr
      return ["AF_TTY", 0, "tty", "tty"]
    end

    def shutdown
    end

    def close
      STDIN.close
      STDOUT.close
    end

    def fcntl(cmd, arg)
    end
  end

  class NullObject
    def initialize(*args)
    end

    def method_missing(mid, *args)
      return self
    end

    def self.method_missing(mid, *args)
      return new
    end
  end

  begin
    require "progressbar"
  rescue LoadError
    class ProgressBar < NullObject
      VERSION = "not available"
    end
  end

  class Option
    def initialize(name, description)
      @name = name
      @description = description
    end

    def opt_name
      return @name.tr("_", "-")
    end

    def arg_name
      return @name.slice(/[a-z]*\z/ni).upcase
    end
  end

  class BoolOption < Option
    def define(opts, config)
      opt = "--[no-]" + opt_name
      opts.define(opt, @description) do |arg|
        config[@name] = arg
      end
    end
  end

  class IntOption < Option
    def define(opts, config)
      opt = "--" + opt_name + "=" + arg_name
      opts.define(opt, Integer, @description) do |arg|
        config[@name] = arg
      end
    end
  end

  class StringOption < Option
    def define(opts, config)
      opt = "--" + opt_name + "=" + arg_name
      opts.define(opt, @description) do |arg|
        config[@name] = arg
      end
    end
  end

  class ArrayOption < Option
    def define(opts, config)
      s = arg_name[0, 1]
      args = ("1".."3").collect { |i| s + i.to_s }.join(",")
      opt = "--" + opt_name + "=" + args
      opts.define(opt, Array, @description) do |arg|
        config[@name] = arg
      end
    end
  end

  class Action < Option
    def define(opts, config)
      opt = "--" + opt_name
      opts.define(opt, @description) do
        config["action"] = @name
      end
    end
  end

  OPTIONS = [
    StringOption.new("config_file", "path to .ximapd"),
    IntOption.new("port", "port"),
    StringOption.new("user", "user"),
    StringOption.new("password", "password"),
    StringOption.new("data_dir", "data directory"),
    StringOption.new("plugin_path", "path for plugins"),
    StringOption.new("db_type", "database type (yaml, pstore)"),
    IntOption.new("max_clients", "max number of clients"),
    BoolOption.new("ssl", "use SSL"),
    StringOption.new("ssl_key", "path to SSL private key"),
    StringOption.new("ssl_cert", "path to SSL certificate"),
    StringOption.new("remote_host", "host of remote IMAP server"),
    IntOption.new("remote_port", "port of remote IMAP server"),
    BoolOption.new("remote_ssl", "use SSL for remote IMAP server"),
    StringOption.new("remote_auth", "auth type of remote IMAP server"),
    StringOption.new("remote_user", "user of remote IMAP server"),
    StringOption.new("remote_password", "password of remote IMAP server"),
    BoolOption.new("import_all", "import all mails by --import-imap"),
    BoolOption.new("import_imap_flags", "import IMAP flags by --import-imap"),
    BoolOption.new("keep", "keep retrieved mails on the remote server"),
    StringOption.new("dest_mailbox", "destination mailbox name"),
    ArrayOption.new("ml_header_fields",
                    "header fields to handle same as X-ML-Name"),
    StringOption.new("default_charset", "default value for charset"),
    StringOption.new("log_level",
                     "log level (fatal, error, warn, info, debug)"),
    IntOption.new("log_shift_age", "number of old log files to keep"),
    IntOption.new("log_shift_size", "max logfile size"),
    IntOption.new("sync_threshold_chars",
                  "number of characters to start index sync"),
    BoolOption.new("debug", "turn on debug mode"),
    BoolOption.new("verbose", "turn on verbose mode"),
    BoolOption.new("progress", "show progress"),
  ]

  ACTIONS = [
    Action.new("start", "start daemon"),
    Action.new("stop", "stop daemon"),
    Action.new("import", "import mail"),
    Action.new("import_mbox", "import mbox"),
    Action.new("import_imap", "import from another imap server"),
    Action.new("rebuild_index", "rebuild index"),
    Action.new("interactive", "interactive mode"),
    Action.new("version", "print version"),
    Action.new("help", "print this message"),
  ]
end

if $0 == __FILE__
  File.umask(0077)
  imapd = Ximapd.new
  imapd.run(ARGV)
end

# vim: set filetype=ruby expandtab sw=2 :

