// smtpd/Session.cc
// This file is part of Decimail; see http://decimail.org
// (C) 2004 Philip Endecott
 
// This is version $Name$
//   (if there is no version (e.g. V0-1) mentioned in the previous line,
//    this is probably a snapshot from between "official" releases.)
 
// 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.
 
#define _GNU_SOURCE 1

#include "Session.hh"

#include "Configuration.hh"
#include "Exception.hh"
#include "MessageStr.hh"
#include "utils.hh"
#include "SmtpClient.hh"
#include "MessageDb.hh"
#include "ip.hh"

#include <list>
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <time.h>
#include <syslog.h>
#include <unistd.h>
using namespace std;


namespace Smtpd {

  Session::Session(int i, int o):
    num(next_num++), in_fd(i), out_fd(o)
  {}


  Session::~Session()
  {
    if (smtpc.is_connected()) {
      smtpc.disconnect();
    }
  }


  class SmtpException: public Exception {
  public:
    const string msg;
    SmtpException(string m): msg(m) {}
    void report(ostream& s) { s << "SMTP error " << msg << endl; }
  };


  static string own_domain;
  static string own_hostname;
  static string default_mail_domain;
  static bool hostname_valid=false;

  static void find_own_hostname(void)
  {
    if (hostname_valid) {
      return;
    }

    string fqdn = get_own_hostname();
    unsigned int dot_pos=fqdn.find('.');
    if (dot_pos==fqdn.npos) {
      own_domain="unknown.domain";
      own_hostname=fqdn;
    } else {
      own_domain=fqdn.substr(dot_pos+1);
      own_hostname=fqdn.substr(0,dot_pos);
    }
    default_mail_domain = own_domain;
    hostname_valid=true;
  }


  void Session::run(void)
  {
    srandom(time(NULL));
    find_own_hostname();

    find_client_ip();

    bool local = (client_ip.s_addr==htonl(INADDR_LOOPBACK));

    in_f = fdopen(in_fd,"r");
    if (!in_f) {
      throw SysException("fdopen");
    }
    out_f = fdopen(out_fd,"w");
    if (!out_f) {
      throw SysException("fdopen");
    }

    string ehlo_domain;
    string sender;
    list<string> outgoing_recipients;
    list<pair<string,string> > incoming_recipients;
    string data;

    if (local) {
      wr("220 "+own_hostname+" decimail smtpd service ready (outgoing allowed)\r\n");
    } else {
      wr("220 "+own_hostname+" decimail smtpd service ready (incoming only)\r\n");
    }

    enum state_t { st_ehlo, st_mail, st_rcpt, st_finished };
    state_t state = st_ehlo;
    
    char* linep = NULL;
    size_t linelen = 0;

    while (state != st_finished) {
      fflush(out_f);
      int rc = getline(&linep,&linelen,in_f);
      if (rc==-1) {
	break;
      }
	
      syslog(LOG_MAIL|LOG_DEBUG,"{%d} C: %s",num,linep);

      if (strncasecmp(linep,"EHLO ",5)==0) {

	if (state==st_ehlo) {
	  char* domp = NULL;
	  sscanf(linep+5,"%as",&domp);
	  ehlo_domain=domp;
	  free(domp);
	  wr("250-"+own_domain+" hello\r\n");
	  wr("250 8BITMIME\r\n");
	  state = st_mail;
	} else {
	  wr("503 Bad sequence of commands\r\n");
	}

      } else if (strncasecmp(linep,"HELO ",5)==0) {
	
	if (state==st_ehlo) {
	  char* domp = NULL;
	  sscanf(linep+5,"%as",&domp);
	  ehlo_domain=domp;
	  free(domp);
	  wr("250 "+own_domain+" hello\r\n");
	  state = st_mail;
	} else {
	  wr("503 Bad sequence of commands\r\n");
	}

      } else if (strncasecmp(linep,"MAIL FROM:",10)==0) {
	
	if (state==st_mail) {
	  char* sndrp = NULL;
	  char* addr_start = NULL;
	  if (linep[10]=='<') {
	    addr_start=&linep[11];
	  } else if (linep[10]==' ' && linep[11]=='<') {
	    addr_start=&linep[12];
	  }
	  if (addr_start) {
	    if (addr_start[0]=='>') {
	      sender="";
	    } else {
	      sscanf(addr_start,"%a[^>]",&sndrp);
	      sender=sndrp;
	      free(sndrp);
	    }
	    wr("250 OK\r\n");
	    state = st_rcpt;
	  } else {
	    wr("500 Syntax Error\r\n");
	  }
	} else {
	  wr("503 Bad sequence of commands\r\n");
	}

      } else if (strncasecmp(linep,"RCPT TO:",8)==0) {
	
	if (state==st_rcpt) {
	  char* rcptp = NULL;
	  char* addr_start = NULL;
	  if (linep[8]=='<') {
	    addr_start=&linep[9];
	  } else if (linep[8]==' ' && linep[9]=='<') {
	    addr_start=&linep[10];
	  }
	  if (addr_start) {
	    // need glibc, not C99, for %a[
	    sscanf(addr_start,"%a[^>]",&rcptp);
	    string rcpt = rcptp;
	    free(rcptp);

	    if (rcpt.find('@')==rcpt.npos) {
	      rcpt.append('@'+default_mail_domain);
	    }
	    
	    string user = lookup_local_user(rcpt);
	    if (user!="") {
	      incoming_recipients.push_back(make_pair(rcpt,user));
	      wr("250 OK (local recipient)\r\n");
	    } else {
	      if (local) {
		outgoing_recipients.push_back(rcpt);
		wr("250 OK (remote recipient)\r\n");
	      } else {
		wr("550 No relaying\r\n");
	      }
	    }
	  } else {
	    wr("500 Syntax Error\r\n");
	  }
	} else {
	  wr("503 Bad sequence of commands\r\n");
	}

      } else if (strncasecmp(linep,"DATA",4)==0) {
	
	if (state==st_rcpt) {
	  if (incoming_recipients.empty() && outgoing_recipients.empty()) {
	    wr("554 No recipients\r\n");
	  } else {
	    wr("354 Ready for input\r\n");
	    fflush(out_f);
	    char* dlp = NULL;
	    size_t dll = 0;
	    data.clear();
	    int n;
	    while (1) {
	      n = getline(&dlp,&dll,in_f);
	      if (n==-1) {
		break;
	      }
	      if (dlp[0]=='.') {
		if ((dlp[1]=='\r')||(dlp[1]=='\n')) {
		// should not really allow \n above, but help interactive debug
		  break;
		} else {
		  data.append(dlp+1,n-1);
		}
	      } else {
		data.append(dlp,n);
	      }
	    }
	    if (n==-1) {
	      break;
	    }
	    try {
	      try {
		for(list<string>::const_iterator i = outgoing_recipients.begin();
		    i!=outgoing_recipients.end(); ++i) {
		  process_outgoing(ehlo_domain,sender,*i,data);
		}
		for(list<pair<string,string> >::const_iterator i =
		      incoming_recipients.begin();
		    i!=incoming_recipients.end(); ++i) {
		  process_incoming(ehlo_domain,sender,i->first,i->second,data);
		}
		wr("250 OK message received\r\n");
		incoming_recipients.clear();
		outgoing_recipients.clear();
		state = st_mail;
	      }
	      RETHROW_MISC_EXCEPTIONS;
	    }
	    catch(SmtpException& E) {
	      wr(E.msg+"\r\n");
	      incoming_recipients.clear();
	      outgoing_recipients.clear();
	      state = st_mail;
	    }
	    catch(Exception& E) {
	      // Possible exceptions include "malformed message" due to no
	      // Date: header.
	      wr("451-An error occured while processing the message\r\n");
	      ostringstream ss;
	      E.report(ss);
	      wr("451-"+ss.str()+"\r\n");
	      wr("451 The connection will now close\r\n");
	      state = st_finished;
	    }
	  }
	} else {
	  wr("503 Bad sequence of commands\r\n");
	}

      } else if (strncasecmp(linep,"QUIT",4)==0) {
	wr("221 Goodbye\r\n");
	state = st_finished;

      } else if (strncasecmp(linep,"RSET",4)==0) {
	wr("250 OK\r\n");
	state = st_mail;

      } else if (strncasecmp(linep,"NOOP",4)==0) {
	wr("250 OK\r\n");

      } else if (strncasecmp(linep,"VRFY",4)==0) {
	wr("502 Command not implemented\r\n");

      } else if (strncasecmp(linep,"EXPN",4)==0) {
	wr("502 Command not implemented\r\n");

      } else if (strncasecmp(linep,"HELP",4)==0) {
	wr("502 Command not implemented\r\n");

      } else {
	wr("500 Syntax error\r\n");
      }

    }

    free(linep);
    fflush(out_f);
    fclose(in_f);
    fclose(out_f);
  }


  void Session::find_client_ip(void)
  {
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int rc = getpeername(in_fd, (struct sockaddr*)&client_addr,
			 &client_addr_len);
    if (rc==-1) {
      if (errno==ENOTSOCK) {
	client_ip.s_addr=htonl(INADDR_LOOPBACK);
      } else {
	throw SysException("getpeername()");
      }
    } else {
      assert (client_addr.sin_family==AF_INET);
      client_ip=client_addr.sin_addr;
    }
  }


  void Session::wr(string s)
  {
    syslog(LOG_MAIL|LOG_DEBUG,"{%d} S: %s",num,s.c_str());
    unsigned int n = fwrite(s.data(),sizeof(char),s.size(),out_f);
    if (n<s.size()) {
      throw SysException("fwrite");
    }
  }


  string Session::lookup_local_user(string recipient)
  {
    DmDatabase::Query q(&db);
    q << "select username from incoming_addresses where "
      << DmDatabase::Query::qs(recipient) << " ilike address_pat;";
    q.run();
      
    if (q.get_ntuples()==0) {
      return "";
    } else if (q.get_ntuples()==1) {
      return q.get(0,0);
    } else {
      throw "Multiple incoming_addresses match the same address";
    }
  }


  static string rfc822time(void)
  {
    char s[36];
    time_t t;
    time(&t);
    struct tm tm;
    localtime_r(&t,&tm);
    strftime(s,sizeof(s),"%a, %d %b %Y %H:%M:%S %z",&tm);
    return s;
  }


  void Session::find_matching_addrs(const Message::email_address_list& l,
				    string username,
				    list<string>& matches)
  {
    for(Message::email_address_list::const_iterator i=l.begin();
	i!=l.end(); ++i) {
      if (i->isGroup()) {
	// ?
      } else {
	string addr = i->mailbox().mailbox()+'@'+i->mailbox().domain();
	DmDatabase::Query q(&db);
	q << "select 1 from incoming_addresses "
	  << "where username=" << DmDatabase::Query::qs(username)
	  << " and " << DmDatabase::Query::qs(addr) << " ilike address_pat;";
	q.run();
	if (q.get_ntuples()>0) {
	  matches.push_back(addr);
	}
      }
    }
  }

  
  string Session::find_new_sender_for_reply(const mimetic::MimeEntity& m,
					    string username)
  {
    // m is a message about to be sent.
    // Find an original message that this is a reply to, and find
    // the address that it was sent to.
    // This address will be used as the from: address in this message.
    // If it is not a reply to an earlier message, or an address can't be
    // found for some other reason, return "".
    
    if (!m.header().hasField("In-Reply-To")) {
      return "";
    }

    string orig_rfc822_messageid = m.header().field("In-Reply-To").value();

    DmDatabase::Query q(&db);
    // need to put real owner in here
    q << "select msg_id from messages where owner="
      << DmDatabase::Query::qs(username)
      << " and rfc822_messageid="
      << DmDatabase::Query::qs(orig_rfc822_messageid) << ";";
    q.run();
    if (q.get_ntuples()==0) {
      return "";
    }
    int orig_msg_id = q.get_num(0,0);

    MessageDb orig_msg(db, orig_msg_id);
    list<string> orig_addrs;
    find_matching_addrs(orig_msg.get_to(),username,orig_addrs);
    find_matching_addrs(orig_msg.get_cc(),username,orig_addrs);
    find_matching_addrs(orig_msg.get_bcc(),username,orig_addrs);
    if (orig_addrs.empty()) {
      return "";
    }
	
    return orig_addrs.front();
  }


  string Session::lookup_auto_username(string sender)
  {
    DmDatabase::Query q(&db);
    q << "select username from auto_users where "
      << "addr=" << DmDatabase::Query::qs(sender) << ";";
    q.run();
    if (q.get_ntuples()==0) {
      return "";
    } else {
      return q.get(0,0);
    }
  }



  string Session::find_custom_addr(string recipient, string username)
  {
    DmDatabase::Query q(&db);
    q << "select custom_addr from custom_addrs where "
      << "username=" << DmDatabase::Query::qs(username) << " and "
      << DmDatabase::Query::qs(recipient) << " ilike correspondent_pat;";
    q.run();
    if (q.get_ntuples()==0) {
      return "";
    } else {
      return q.get(0,0);
    }
  }


  string Session::random_address(string username)
  {
    DmDatabase::Query q(&db);
    q << "select random_template from auto_users where username="
      << DmDatabase::Query::qs(username) << ";";
    q.run();
    string a = q.get(0,0);

    for(string::iterator i=a.begin();
	i!=a.end(); ++i) {
      if (*i == '#') {
	*i = 'a' + (random()%26);
      }
    }
    return a;
  }


  void Session::process_outgoing(string ehlo_domain,
				 string sender, string recipient,
				 string data)
  {
    // need something generic here

    string username = lookup_auto_username(sender);

    if (username!="") {

      mimetic::MimeEntity m;
      istringstream s(data);
      m.load(s);

      string new_sender = find_new_sender_for_reply(m,username);
      
      if (new_sender=="") {
	
	new_sender = find_custom_addr(recipient,username);

	if (new_sender=="") {
	  new_sender = random_address(username);
	}
      }

      syslog(LOG_MAIL|LOG_DEBUG,"{%d} Substituting %s for %s",num,
	     new_sender.c_str(),sender.c_str());

      mimetic::MailboxList::const_iterator orig_from_mbx_i
	= m.header().from().begin();
      if (orig_from_mbx_i != m.header().from().end()) {
	mimetic::Mailbox from_mbx(new_sender);
	from_mbx.label(orig_from_mbx_i->label());
	mimetic::MailboxList l;
	l.push_back(from_mbx);
	m.header().from(l);
      }

      ostringstream os;
      os << m;
      data = os.str();
      sender = new_sender;
    }
      

    ostringstream newheaders;
    newheaders << "Received: from " << ehlo_domain
	       << " ([" << client_ip << "])"
	       << " by " << own_hostname << "\r\n"
	       << "  with smtp (dmsmtpd 0.00001)"
	       << "; " << rfc822time()
	       << "\r\n";
    
    string msg = newheaders.str() + data;

    if (!smtpc.is_connected()) {
      const string smarthost =
	Configuration::singleton().get_str("smtpd_outgoing_smarthost");
      smtpc.connect(smarthost,own_domain);
    }

    smtpc.send_msg(sender,recipient,msg);
  }


  static void save_backup(string data, int msg_id);

  
  void Session::process_incoming(string ehlo_domain,
				 string sender, string recipient,
				 string username, string data)
  {
    int msg_id = db.get_next_uid();
	     
    ostringstream newheaders;
    newheaders << "Return-Path: <" << sender << ">\r\n"
	       << "Received: from " << ehlo_domain
	       << " ([" << client_ip << "])"
	       << " by " << own_hostname << "\r\n"
	       << "  id <" << msg_id << "@" << own_hostname << ">\r\n"
	       << "  with smtp (dmsmtpd 0.00001)\r\n"
	       << "  for <" << recipient << "> (" << username << ") "
	       << "; " << rfc822time()
	       << "\r\n";
    
    string msg = newheaders.str() + data;

    save_backup(msg,msg_id);
    MessageStr m(msg, msg_id);
    db.insert(m,username); // need to insert with NOW as (internal)date
  }


  static void save_backup(string data, int msg_id)
  {
    assert(msg_id<1000000);
    char numstr[7];
    sprintf(numstr,"%06d",msg_id);
    string fn = Configuration::singleton().get_str("message_backup_dir");
    //string fn = "/var/local/decimail/messages/";
    fn += numstr[0];
    fn += numstr[1];
    int rc = mkdir(fn.c_str(),0777);
    if (rc==-1) {
      if (errno!=EEXIST) {
	throw SysException("mkdir("+fn+")");
      }
    }
    fn += "/";
    fn += numstr[2];
    fn += numstr[3];
    rc = mkdir(fn.c_str(),0777);
    if (rc==-1) {
      if (errno!=EEXIST) {
	throw SysException("mkdir("+fn+")");
      }
    }
    fn += "/";
    fn += numstr[4];
    fn += numstr[5];
    
    FILE* f = fopen(fn.c_str(),"w");
    if (!f) {
      throw SysException("fopen("+fn+")");
    }
    unsigned int n = fwrite(data.data(),sizeof(char),data.size(),f);
    if (n<data.size()) {
      SysException("fwrite("+fn+")");
    }
    rc = fclose(f);
    if (rc) {
      throw SysException("fclose("+fn+")");
    }
  }

  int Session::next_num=0;

};
