// imapd/Session.cc
// This file is part of Decimail; see http://decimail.org
// (C) 2004 Philip Endecott
 
 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
#include "Session.hh"

#include "CommandParser.hh"
#include "Configuration.hh"

#include "utils.hh"
#include "select.hh"

#include <algorithm>
using namespace std;

#ifdef USE_IDENT
#include <ident.h>
#endif

#include <syslog.h>


namespace Imapd {

  void CloseConnection::report(ostream& strm)
  {
    strm << "Connection close requested" << endl;
  }


  Session::Session(FileDescriptor& i, FileDescriptor& o):
    num(next_num++), in_fd(i), out_fd(o),
    out_strm(o),
    state(non_auth), unreported_messages(false)
  {
    parser=new CommandParser(*this);
  }


  Session::~Session()
  {
    delete parser;
    flush();
    for(map<int,Message*>::iterator i=cached_messages.begin();
	i!=cached_messages.end(); ++i) {
      delete i->second;
    }
  }


  bool Session::wait_for_imap_or_db(void)
  {
    // Wait for something to happen on either the IMAP input or the
    // database connection (i.e. a notification).
    // Return TRUE for imap, FALSE for db.

    int readable_fd = wait_until(db.get_fd().readable() || in_fd.readable()); 
    return in_fd == readable_fd;
  }


  void Session::run(void)
  {
    bool preauth = false;

#ifdef USE_IDENT
    char* ident_cp = ident_id(in_fd,2);
    if (ident_cp) {
      set_username(ident_cp);
      db.listen_for_new_messages(username);
      set_state(auth);
      free(ident_cp);
      respond_untagged("PREAUTH decimail ready ("+username+")");
      preauth = true;
    }
#endif

    if (!preauth) {
      respond_untagged("OK decimail ready");
    }

    flush();

    in_fd.set_nonblocking();

    try {
      while (1) {

	// if unreported_messages is true, we should also
	// put "imap_out writable" in the select, so we can
	// retry the unilateral write when this becomes true.
	if (wait_for_imap_or_db()) {
	  // imap input ready
	  parser->parse(in_fd);
	} else {
	  // database notification pending
	  send_any_unilateral_updates(false);
	}
      }
    }
    catch(CloseConnection) {
      return;
    }
  }

  
  void Session::send_any_unilateral_updates(bool block)
  {
    if (state!=selected) {
      db.check_for_new_messages();  // to clear notification
      return;
    }

    if (db.check_for_new_messages()) {
      get_msg_ids();
      unreported_messages=true;
    } else {
      if (!unreported_messages) {
	return;
      }
    }

    int exists = seqnum_to_msgid.size()-1;
    int recent = exists;
    
    if (!block) {
      out_fd.set_nonblocking();
    }

    try {
      // Perhaps we should use a single write() for each response;
      // this scheme could give up after filling a buffer with half of
      // a response, which would be messy.
      // (But many responses e.g. fetch responses benefit from the
      // buffering of a stream layer)
      respond_untagged(int_to_string(exists)+" EXISTS");
      respond_untagged(int_to_string(recent)+" RECENT");
      flush();
      unreported_messages=false;
    }
    catch (SysException& E) {
      if (E.get_errno()!=EWOULDBLOCK) {
	throw;
      }
    }

    if (!block) {
      out_fd.set_blocking();
    }

    // other things to worry about: flags changing, messages deleted/expunged
  }


  void Session::get_msg_ids(void)
  {
    DmDatabase::Query q(&get_db());
    q << get_mailbox_query() << " order by msg_id;";
    q.run();
    seqnum_to_msgid.resize(q.get_ntuples()+1);
    msgid_to_seqnum.clear();
    for(int seqnum=1; seqnum<=q.get_ntuples(); ++seqnum) {
      int msgid = q.get_num(seqnum-1,0);
      seqnum_to_msgid[seqnum]=msgid;
      msgid_to_seqnum[msgid]=seqnum;
    }
  }


  class NotAllowedInThisState: public RespondBad {
  public:
    NotAllowedInThisState(void):
      RespondBad("Command not allowed in this state") {}
  };


  void Session::check_state(int allowed_states)
  {
    if (!(allowed_states&state)) {
      throw NotAllowedInThisState();
    }
  }

  void Session::respond_ok(string tag, string msg)
  {
    respond(tag,"OK",msg);
  }

  void Session::respond_no(string tag, string msg)
  {
    respond(tag,"NO",msg);
  }

  void Session::respond_bad(string tag, string msg)
  {
    respond(tag,"BAD",msg);
  }

  void Session::respond_untagged(string msg)
  {
    if (Configuration::singleton().get_bool("imapd_log_imap",false)) {
      syslog(LOG_MAIL|LOG_DEBUG,"{%d} S: * %s",num,msg.c_str());
    }
    out_strm << "* " << msg << "\r\n";
  }

  void Session::respond(string tag, string response, string msg)
  {
    if (Configuration::singleton().get_bool("imapd_log_imap",false)) {
      syslog(LOG_MAIL|LOG_DEBUG,"{%d} S: %s %s %s",num,tag.c_str(),response.c_str(),msg.c_str());
    }
    out_strm << tag << " " << response << " " << msg << "\r\n";
  }

  void Session::send_continuation_request(void)
  {
    out_strm << "+ continue\r\n" << std::flush;
  }

  void Session::flush(void)
  {
    out_strm << std::flush;
  }


  void Session::set_mailbox(string m)
  {
    mailbox = m;
    mailbox_query = get_db().get_mailbox_query(get_username(),m);
  }

  
  template<typename T, typename U>
  struct contained_in_map {
    const map<T,U>& the_map;
    contained_in_map(const map<T,U>& m): the_map(m) {}
    bool operator()(const T& t) { return the_map.find(t) != the_map.end(); }
  };


  void Session::prefetch_messages(const list<int>& msg_ids)
  {
    list<int> new_msg_ids;
    remove_copy_if(msg_ids.begin(), msg_ids.end(), back_inserter(new_msg_ids),
		   contained_in_map<int,Message*>(cached_messages));

    auto_ptr<list<Message*> > new_msgs = db.get_messages(new_msg_ids);

    for(list<Message*>::const_iterator i=new_msgs->begin();
	i!=new_msgs->end(); ++i) {
      cached_messages[(*i)->get_id()] = (*i);
    }
  }


  int Session::next_num=0;

};
