#!/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 "monitor"
require "net/imap"
require "pstore"
require "yaml/store"
require "find"
require "fileutils"
require "bdb"
require "digest/md5"
require "iconv"
require "nkf"
require "date"
require "set"
require "logger"
require "tmail"
require "rast"
require "optparse"

now = DateTime.now
unless defined?(now.to_time)
  class DateTime
    def to_time
      d = new_offset(0)
      d.instance_eval do
        Time.utc(year, mon, mday, hour, min, sec,
                 (sec_fraction * 86400000000).to_i)
      end.
          getlocal
    end
  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
  ProgressBar = NullObject
end

Thread.abort_on_exception = true

module TMail
  def Decoder.decode(str, encoding = nil)
    return str
  end
end

module Rast
  if !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.1"

  LOG_SHIFT_AGE = 10
  LOG_SHIFT_SIZE = 1 * 1024 * 1024

  @@debug = false

  def self.debug
    return @@debug
  end

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

  def initialize
    @args = nil
    @config = {}
    @server = nil
    @logger = nil
    @threads = []
    @sessions = {}
    @monitor = Monitor.new
    @option_parser = OptionParser.new { |opts|
      opts.banner = "usage: #{File.basename($0)} [options]"
      opts.separator("")
      opts.separator("options:")
      define_options(opts, @config, OPTIONS)
      opts.separator("")
      define_options(opts, @config, ACTIONS)
    }
  end

  def run(args)
    @args = args
    parse_options(@args)
    @server = nil
    @logger = open_logger
    @config["logger"] = @logger
    @threads = []
    @sessions = {}
    send(@config["action"] || "help")
  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_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)
    rescue
      raise if @config["debug"]
      STDERR.printf("%s: %s\n", $0, $!)
      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
    if @config["action"] != "start"
      for m in [:fatal, :error, :warn]
        class << logger; self; end.send(:define_method, m) do |s|
          STDERR.printf("%s: %s\n", $0, s)
          super(s)
        end
      end
    end
    return logger
  end

  def start
    @server = open_server
    @logger.info("started")
    daemon if !@config["debug"]
    @main_thread = Thread.current
    Signal.trap("TERM", &method(:terminate))
    Signal.trap("INT", &method(:terminate))
    begin
      loop do
        begin
          sock = @server.accept
        rescue Exception
          if @config["ssl"] && $!.kind_of?(OpenSSL::SSL::SSLError)
            retry
          else
            raise
          end
        end
        Thread.start(sock) do |socket|
          session = Session.new(@config, sock, @monitor)
          @sessions[Thread.current] = session
          session.start
          @sessions.delete(Thread.current)
        end
      end
    rescue SystemExit
      raise
    rescue Exception
      @logger.error("#{$!.class}: #{$!}")
      for line in $@
        @logger.error("  #{line}")
      end
    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\n", 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 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("/")
    STDIN.reopen("/dev/null")
    STDOUT.reopen("/dev/null", "w")
    path = File.expand_path("stderr", @config["data_dir"])
    STDERR.reopen(path, "w")
  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}")
      @monitor.synchronize do
        @logger.debug("raise TerminateException to #{t}")
        t.raise(TerminateException.new)
        @logger.debug("raised TerminateException to #{t}")
      end
      begin
        @logger.debug("join #{t}")
        t.join
        @logger.debug("joined #{t}")
      rescue SystemExit
        raise
      rescue Exception
        @logger.error($!)
      end
      @logger.debug("closed session \##{i}")
      i += 1
    end
  end

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

  def open_mail_store
    mail_store = Ximapd::MailStore.new(@config)
    mail_store.lock
    if @config["dest_mailbox"] &&
      !mail_store.mailboxes.include?(@config["dest_mailbox"])
      mail_store.create_mailbox(@config["dest_mailbox"])
    end
    begin
      yield(mail_store)
    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_STATUS = {
    "uidvalidity" => 1,
    "last_uid" => 0,
    "last_mailbox_id" => 1
  }
  DEFAULT_MAILBOXES = {
    "INBOX" => {
      "flags" => "",
      "id" => 1,
      "query" => "mailbox-id = 1",
      "last_peeked_uid" => 0
    },
    "ml" => {
      "flags" => "\\Noselect"
    },
    "queries" => {
      "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 MailStore
    include QueryFormat

    attr_reader :flags_db
    attr_accessor :read_only

    def initialize(config)
      @config = config
      @logger = @config["logger"]
      @path = File.expand_path(@config["data_dir"])
      @mail_path = File.expand_path("mails", @path)
      @db_path = File.expand_path("db", @path)
      FileUtils.mkdir_p(@path)
      FileUtils.mkdir_p(@mail_path)
      FileUtils.mkdir_p(@db_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
      @mailbox_db.transaction do
        @mailbox_db["status"] ||= DEFAULT_STATUS.dup
        @mailbox_db["mailboxes"] ||= DEFAULT_MAILBOXES.dup
        @mailbox_db["mailing-lists"] ||= {}
      end
      @db_env = BDB::Env.new(@db_path,
                             BDB::CREATE | BDB::INIT_MPOOL | BDB::INIT_LOG)
      @flags_db = BDB::Recno.open("flags.db", nil, BDB::CREATE,
                                  "env" => @db_env)
      @index_path = File.expand_path("index", @path)
      if !File.exist?(@index_path)
        Rast::DB.create(@index_path, INDEX_OPTIONS)
      end
      @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 = nil
      @index_ref_count = 0
      lock_path = File.expand_path("lock", @path)
      @lock = File.open(lock_path, "w+")
      @lock_count = 0
      @read_only = false
    end

    def close
      @mailbox_db.transaction do
        @last_peeked_uids.each do |name, uid|
          @mailbox_db["mailboxes"][name]["last_peeked_uid"] = uid
        end
      end
      @flags_db.close
      @lock.close
    end

    def lock
      if @lock_count == 0
        @lock.flock(File::LOCK_EX)
      end
      @lock_count += 1
    end

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

    def sync
      @flags_db.sync
    end

    def mailboxes
      @mailbox_db.transaction 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)
      mailbox = nil
      status = nil
      @mailbox_db.transaction do
        mailboxes = @mailbox_db["mailboxes"]
        mailbox = mailboxes[mailbox_name]
        status = @mailbox_db["status"]
      end
      if mailbox.nil?
        raise NoMailboxError.new("no such mailbox")
      end
      if /\\Noselect/ni.match(mailbox["flags"])
        raise NotSelectableMailboxError.new("can't open #{mailbox_name}: not a selectable mailbox")
      end
      open_index do |index|
        mailbox_status = MailboxStatus.new
        result = index.search(mailbox["query"],
                              "properties" => ["uid"],
                              "start_no" => 0)
        mailbox_status.messages = result.hit_count
        mailbox_status.unseen = result.items.select { |i|
          !/\\Seen\b/ni.match(@flags_db[i.properties[0]])
        }.length
        query = format("%s uid > %d",
                       mailbox["query"], mailbox["last_peeked_uid"])
        result = index.search(query,
                              "properties" => ["uid"],
                              "start_no" => 0,
                              "num_items" => Rast::RESULT_MIN_ITEMS)
        mailbox_status.recent = result.hit_count
        mailbox_status.uidnext = status["last_uid"] + 1
        mailbox_status.uidvalidity = status["uidvalidity"]
        if !@read_only
          @last_peeked_uids[mailbox_name] = status["last_uid"]
        end
        return mailbox_status
      end
    end

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

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

    def import_file(f, mailbox_name = nil)
      return import_mail(f.read, mailbox_name)
    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)
              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"]
      import_all = @config["import_all"]

      @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]
              next if mail_count == 0
              #imported_uids = []
              imported_uids = Set.new
              progress_bar = nil
              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(index,
                                         mail.attr["BODY[]"], mailbox_name,
                                         flags, indate)
                    #imported_uids.push(mail.attr["UID"])
                    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 import_all
                  @logger.info("#{mail_count} messages in #{folder.name}")
                  if @config["verbose"]
                    progress_bar = ProgressBar.new(folder.name, mail_count)
                  else
                    progress_bar = NullObject.new
                  end
                  imap.fetch(1 .. -1, fetch_attrs)
                else
                  uids = imap.uid_search(["UNSEEN"])
                  @logger.info("#{uids.length} unseen messages " + 
                               "in #{folder.name}")
                  if @config["verbose"]
                    progress_bar = ProgressBar.new(folder.name, uids.length)
                  else
                    progress_bar = NullObject.new
                  end
                  while uids.length > 0
                    imap.uid_fetch(uids.slice!(0, 100), fetch_attrs)
                  end
                end
              ensure
                imap.remove_response_handler(handler)
              end
              #while imported_uids.length > 0
              #  imap.uid_store(imported_uids.slice!(0, 100),
              #                 "+FLAGS.SILENT", [:Seen])
              #end
            rescue
              @logger.error("#{$!}: #{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 = "")
      open_index do |index|
        return import_mail_internal(index, str, mailbox_name, flags)
      end
    end

    def uid_search(mailbox_name, query)
      mailbox = get_mailbox(mailbox_name)
      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
        }
        #if mailbox_name != "INBOX"
        #  query += " " + mailbox["query"]
        #end
        query += " " + mailbox["query"]
        result = index.search(query, options)
        return result.items.collect { |i| i.properties[0] }
      end
    end

    def fetch(mailbox_name, sequence_set)
      mailbox = get_mailbox(mailbox_name)
      open_index do |index|
        result = index.search(mailbox["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
              mail = mailbox.get_mail(i, result.items[i - 1].properties[0])
              mail.internal_date = result.items[i - 1].properties[1]
              mails.push(mail)
            end
          else
            item = result.items[seq_number - 1]
            next if item.nil?
            uid = item.properties[0]
            mail = mailbox.get_mail(seq_number, uid)
            mail.internal_date = item.properties[1]
            mails.push(mail)
          end
        end
        return mails
      end
    end

    def uid_fetch(mailbox_name, sequence_set)
      mailbox = get_mailbox(mailbox_name)
      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 = mailbox["query"]
        else
          query = mailbox["query"] +
            " ( " + additional_queries.join(" | ") + " )"
        end
        result = index.search(query, options)
        return result.items.collect { |i|
          uid = i.properties[0]
          mail = mailbox.get_mail(uid, uid)
          mail.internal_date = i.properties[1]
          mail
        }
      end
    end

    def get_mailbox(name)
      @mailbox_db.transaction do
        return Mailbox.new(@config, name,
                           @mailbox_db["mailboxes"][name], self)
      end
    end

    def delete_from_index(uid)
      open_index do |index|
        result = index.search("uid = #{uid}")
        result.items.each do |i|
          index.delete(i.doc_id)
        end
      end
    end

    private

    def open_index(flags = Rast::DB::RDWR)
      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

    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 create_mailbox_internal(name, query = nil)
      if @mailbox_db["mailboxes"].key?(name)
        raise MailboxExistError, format("mailbox already exist - %s", name)
      end
      mailbox = {
        "flags" => "",
        "last_peeked_uid" => 0
      }
      if query.nil?
        query = extract_query(name)
        if query.nil?
          @mailbox_db["status"]["last_mailbox_id"] += 1
          query = format('mailbox-id = %d',
                         @mailbox_db["status"]["last_mailbox_id"])
          mailbox["id"] = @mailbox_db["status"]["last_mailbox_id"]
        end
      end
      mailbox["query"] = query
      @mailbox_db["mailboxes"][name] = mailbox
    end

    def extract_query(mailbox_name)
      s = mailbox_name.slice(/\Aqueries\/(.*)/, 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 /.match(line)
          if s
            uid = import_mail_internal(index, s, mailbox_name)
          end
          s = line
        else
          s.concat(line) if s
        end
      end
      if s
        uid = import_mail_internal(index, s, mailbox_name)
      end
    end

    def import_mail_internal(index, str, mailbox_name = nil, flags = "",
                             indate = nil)
      @mailbox_db.transaction do
        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
        s = str.gsub(/\r?\n/, "\r\n").sub(/\AFrom\s+\S+\s+(.*)\r\n/) {
          if indate.nil?
            indate = DateTime.strptime($1 + " " + Time.now.strftime("%z"),
                                       "%a %b %d %H:%M:%S %Y %z")
          end
          ""
        }
        if indate.nil?
          indate = DateTime.now
        end
        @mailbox_db["status"]["last_uid"] += 1
        uid = @mailbox_db["status"]["last_uid"]
        path = get_mail_path(uid, indate)
        FileUtils.mkdir_p(File.dirname(path))
        File.open(path, "w") do |f|
          f.flock(File::LOCK_EX)
          f.print(s)
        end
        time = indate.to_time
        File.utime(time, time, path)
        @flags_db[uid] = flags
        @flags_db.sync
        properties = index_mail(index, uid, s, mailbox_id, indate)
        @logger.info("imported: uid=#{uid} from=<#{properties['from']}> subject=<#{properties['subject']}> date=<#{properties['date']}>")
        return uid
      end
    end

    def get_mail_path(uid, indate)
      relpath = format("%s/%d", indate.strftime("%Y/%m/%d"), uid)
      return File.expand_path(relpath, @mail_path)
    end

    def index_mail(index, uid, mail, mailbox_id, indate)
      properties = Hash.new("")
      properties["uid"] = uid
      properties["size"] = mail.size
      properties["flags"] = ""
      properties["internal-date"] = indate.to_s
      properties["date"] = properties["internal-date"]
      properties["x-mail-count"] = 0
      properties["mailbox-id"] = mailbox_id
      begin
        m = TMail::Mail.parse(mail)
        text = extract_text(m)
        properties = extract_properties(m, properties)
      rescue
        header, body = *mail.split(/^\r\n/)
        text = to_utf8(body, @default_charset)
      end
      index.register(text, properties)
      s = properties["x-ml-name"]
      if !s.empty? && !@mailbox_db["mailing-lists"].key?(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
        @mailbox_db["mailing-lists"][s] = uid
        mailbox_name = format("ml/%s", Net::IMAP.encode_utf7(mbox))
        query = format("x-ml-name = %s", quote_query(properties["x-ml-name"]))
        begin
          create_mailbox_internal(mailbox_name, query)
        rescue MailboxExistError
        end
      end
      return properties
    end

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

    def get_body(mail)
      charset = mail.type_param("charset", @default_charset)
      return to_utf8(mail.body, 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["date"].to_s).to_s
      rescue
      end
      s = nil
      @ml_header_fields.each do |field_name|
        s ||= mail[field_name]
      end
      properties["x-ml-name"] = decode_encoded_word(s.to_s)
      properties["x-mail-count"] = mail["x-mail-count"].to_s.to_i
      if properties["mailbox-id"] == 0 && properties["x-ml-name"].empty?
        properties["mailbox-id"] = 1
      end
      return properties
    end

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

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

  class Mailbox
    attr_reader :name, :mail_store

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

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

    def get_mail(seqno, uid)
      return Mail.new(@config, self, seqno, uid)
    end
  end

  class Mail
    include DataFormat

    attr_reader :mailbox, :seqno, :uid
    attr_accessor :internal_date

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

    def envelope
      mail = parsed_mail
      s = "("
      s.concat(quoted(mail["date"]))
      s.concat(" ")
      s.concat(quoted(mail["subject"]))
      s.concat(" ")
      s.concat(envelope_addrs(mail.from_addrs))
      s.concat(" ")
      s.concat(envelope_addrs(mail.from_addrs))
      s.concat(" ")
      s.concat(envelope_addrs(mail.reply_to_addrs || mail.from_addrs))
      s.concat(" ")
      s.concat(envelope_addrs(mail.to_addrs))
      s.concat(" ")
      s.concat(envelope_addrs(mail.cc_addrs))
      s.concat(" ")
      s.concat(envelope_addrs(mail.bcc_addrs))
      s.concat(" ")
      s.concat(quoted(mail.in_reply_to))
      s.concat(" ")
      s.concat(quoted(mail.message_id))
      s.concat(")")
      return s
    end

    def flags(get_recent = true)
      s = @flags_db[@uid].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)
      @flags_db[@uid] = s
      @flags_db.sync
    end

    def path
      relpath = format("mails/%s/%d", internal_date[0, 10].tr("-", "/"), uid)
      return File.expand_path(relpath, @config["data_dir"])
    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
      @mailbox.mail_store.delete_from_index(@uid)
      begin
        File.unlink(path)
      rescue Errno::ENOENT
        File.unlink(old_path)
      end
    end

    private

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

    def parsed_mail
      if @parsed_mail.nil?
        @parsed_mail = TMail::Mail.parse(to_s)
      end
      return @parsed_mail
    end

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

    def envelope_addr(addr)
      name = addr.phrase
      if addr.routes.empty?
        adl = nil
      else
        adl = addr.routes.collect { |i| "@" + i }.join(",") + ":"
      end
      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.parts.collect { |part|
          body_internal(part)
        }.join
        return format("(%s %s)", parts, quoted(upcase(mail.sub_type)))
      else
        fields = []
        content_type = mail["content-type"]
        if content_type.nil?
          params = "()"
        else
          params = "(" + content_type.params.collect { |k, v|
            format("%s %s", quoted(upcase(k)), quoted(upcase(v)))
          }.join(" ") + ")"
        end
        fields.push(params)
        fields.push("NIL")
        fields.push("NIL")
        content_transfer_encoding =
          (mail["content-transfer-encoding"] || "7BIT").to_s.upcase
        fields.push(quoted(content_transfer_encoding))
        fields.push(mail.body.length.to_s)
        if mail.main_type == "text"
          fields.push(mail.body.to_a.length.to_s)
        end
        return format("(%s %s %s)",
                      quoted(upcase(mail.main_type)),
                      quoted(upcase(mail.sub_type)),
                      fields.join(" "))
      end
    end

    def upcase(s)
      if s.nil?
        return s
      end
      return s.upcase
    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

    @@test = false

    def self.test
      return @@test
    end

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

    def initialize(config, sock, monitor = Monitor.new)
      @config = config
      @sock = sock
      @logger = @config["logger"]
      @parser = CommandParser.new(self, @logger)
      @logout = false
      @peeraddr = nil
      @state = NON_AUTHENTICATED_STATE
      @mail_store = nil
      @current_mailbox = nil
      @examine = false
      @monitor = monitor
    end

    def start
      @peeraddr = @sock.peeraddr[3]
      @logger.info("connect from #{@peeraddr}")
      send_ok("ximapd version %s", VERSION)
      while !@logout
        begin
          begin
            cmd = recv_cmd
          rescue
            send_bad("parse error: %s", $!)
            next
          end
          break if cmd.nil?
          @logger.debug("received #{cmd.name} command from #{@peeraddr}")
          begin
            cmd.exec
          rescue
            raise if @@test
            send_tagged_no(cmd.tag, "%s failed - %s", cmd.name, $!)
            @logger.error("#{$!.class}: #{$!}")
            for line in $@
              @logger.error("  #{line}")
            end
          end
          @logger.debug("sent #{cmd.name} response to #{@peeraddr}")
        rescue TerminateException
          send_data("BYE IMAP server terminating connection")
          break
        end
      end
      synchronize do
        @mail_store.close if @mail_store
        @sock.close
        @logger.info("disconnect from #{@peeraddr}")
      end
    end

    def logout
      @state = LOGOUT_STATE
      @logout = true
    end

    def login
      @mail_store = MailStore.new(@config)
      @state = AUTHENTICATED_STATE
    end

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

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

    def examine?
      return @examine
    end

    def close_mailbox
      @current_mailbox = nil
      @state = AUTHENTICATED_STATE
    end

    def sync
      @mail_store.sync if @mail_store
    end

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

    def recv_cmd
      buf = ""
      loop do
        s = @sock.gets("\r\n")
        break unless s
        buf.concat(s)
        if /\{(\d+)\}\r\n/n =~ s
          send_continue_req("Ready for additional command text")
          s = @sock.read($1.to_i)
          buf.concat(s)
        else
          break
        end
      end
      return nil if buf.length == 0
      @logger.debug(buf.gsub(/^/n, "C: ")) if @config["debug"]
      return @parser.parse(buf)
    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_continue_req(fmt, *args)
      msg = format(fmt, *args)
      send_line("+ " + msg)
    end

    def synchronize
      @monitor.enter
      if @mail_store
        @mail_store.lock
        mail_store_locked = true
      end
      begin
        yield
      ensure
        mail_store.unlock if mail_store_locked
        @monitor.exit
      end
    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
      if !/\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.flags_db, "\\Answered")
      when "DELETED"
        return FlagSearchKey.new(@session.mail_store.flags_db, "\\Deleted")
      when "DRAFT"
        return FlagSearchKey.new(@session.mail_store.flags_db, "\\Draft")
      when "FLAGGED"
        return FlagSearchKey.new(@session.mail_store.flags_db, "\\Flagged")
      when "RECENT", "NEW"
        return FlagSearchKey.new(@session.mail_store.flags_db, "\\Recent")
      when "SEEN"
        return FlagSearchKey.new(@session.mail_store.flags_db, "\\Seen")
      when "UNANSWERED"
        return NoFlagSearchKey.new(@session.mail_store.flags_db, "\\Answered")
      when "UNDELETED"
        return NoFlagSearchKey.new(@session.mail_store.flags_db, "\\Deleted")
      when "UNDRAFT"
        return NoFlagSearchKey.new(@session.mail_store.flags_db, "\\Draft")
      when "UNFLAGGED"
        return NoFlagSearchKey.new(@session.mail_store.flags_db, "\\Flagged")
      when "UNSEEN"
        return NoFlagSearchKey.new(@session.mail_store.flags_db, "\\Seen")
      when "OLD"
        return NoFlagSearchKey.new(@session.mail_store.flags_db, "\\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
      if !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("[Net::IMAP 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("[Net::IMAP 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("[Net::IMAP 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("[Net::IMAP 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("[Net::IMAP 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
    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.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 = @session.mail_store.get_mailbox_status(@mailbox_name)
        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
        @session.send_tagged_no(@tag, "%s", $!)
      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.select(@mailbox_name)
      end
      send_tagged_ok("READ-WRITE")
    end
  end

  class ExamineCommand < MailboxCheckCommand
    private

    def send_tagged_response
      @session.synchronize do
        @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
          @session.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
        @session.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
          @session.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
      if !@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 = @session.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 = @session.mail_store.get_mailbox_status(@mailbox_name)
      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
        @session.mail_store.import_mail(@message, @mailbox_name,
                                        @flags.join(" "))
      end
      send_tagged_ok
    end
  end

  class IdleCommand < Command
    def exec
      @session.sync
      @session.send_continue_req("Waiting for DONE")
      line = @session.recv_line
      send_tagged_ok
    end
  end

  class CloseCommand < Command
    def exec
      @session.synchronize do
        mails = @session.mail_store.fetch(@session.current_mailbox, [1..-1])
        mails.each do |mail|
          if /\\Deleted\b/ni.match(mail.flags(false))
            mail.delete
          end
        end
      end
      @session.close_mailbox
      send_tagged_ok
    end
  end

  class ExpungeCommand < Command
    def exec
      mails = nil
      @session.synchronize do
        mails = @session.mail_store.fetch(@session.current_mailbox, [1..-1])
      end
      seqno = mails.length
      mails.reverse_each do |mail|
        mail = mails[seqno - 1]
        if /\\Deleted\b/ni.match(mail.flags(false))
          @session.synchronize do
            mail.delete
          end
          @session.send_data("%d EXPUNGE", seqno)
        end
        seqno -= 1
      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
        uids = @session.mail_store.uid_search(@session.current_mailbox, query)
      end
      for key in @keys
        uids = key.select(uids)
      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(flags_db, flag)
      @flags_db = flags_db
      @flag_re = Regexp.new(Regexp.quote(flag) + "\\b", true, "n")
    end

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

  class NoFlagSearchKey < FlagSearchKey
    def select(uids)
      return uids.select { |uid|
        !@flag_re.match(@flags_db[uid])
      }
    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
        mails = @session.mail_store.fetch(@session.current_mailbox,
                                          @sequence_set)
      end
      for mail in mails
        data = @atts.collect { |att|
          att.fetch(mail)
        }.join(" ")
        @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)
      if !@atts[0].kind_of?(UidFetchAtt)
        @atts.unshift(UidFetchAtt.new)
      end
    end

    def exec
      mails = nil
      @session.synchronize do
        mails = @session.mail_store.uid_fetch(@session.current_mailbox,
                                              @sequence_set)
      end
      for mail in mails
        data = @atts.collect { |att|
          att.fetch(mail)
        }.join(" ")
        @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)
      datetime = DateTime.strptime(mail.internal_date)
      return format("INTERNALDATE %s",
                    quoted(datetime.strftime("%d-%b-%Y %H:%M:%S %z")))
    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)
          if !/\\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
        mails = @session.mail_store.uid_fetch(@session.current_mailbox,
                                              @sequence_set)
      end
      if !@session.examine?
        for mail in mails
          @session.synchronize do
            @att.store(mail)
          end
          if !@att.silent?
            @session.send_data("%d FETCH (FLAGS (%s))", mail.uid, mail.flags)
          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
        mails = @session.mail_store.uid_fetch(@session.current_mailbox,
                                              @sequence_set)
      end
      for mail in mails
        @session.synchronize do
          @session.mail_store.import_mail(mail.to_s, @mailbox_name)
        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 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("db_type", "database type (yaml, pstore)"),
    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"),
    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"),
  ]

  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("version", "print version"),
    Action.new("help", "print this message"),
  ]
end

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

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

