#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include "lock_maildrop.h"
#include "rw.h"
#include "process.h"
#include "signal.h"
#include "ssl.h"
#include "strlcpy.h"

#include <grp.h>
#include <pwd.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <syslog.h>
#include <errno.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>

#if HAVE_LIBSSL
# include <openssl/rsa.h>
# include <openssl/crypto.h>
# include <openssl/pem.h>
# include <openssl/ssl.h>
# include <openssl/err.h>
#endif

#define STATE_AUTH	1
#define STATE_TRANS	2
#define STATE_UPDATE	3

extern const char * ssl_certfile;
extern const char * ssl_keyfile;
extern const char * local_mbox;
extern char real_username[MAXLINE+1];
extern char real_maildrop[MAXLINE+1];
static char * mdl;

extern const char * mailspool;

int authenticate(char * username, char * password);
void show_uidl(int fd, char * line);

static void do_remove_lock(void) {
  remove_lock(mdl);
}

static void sig_handler(int signo) {
  remove_lock(mdl);
  syslog(LOG_INFO,"%s: %d", "caught signal",signo);
  exit(EXIT_FAILURE);
}

void pop3_session(int fd) {
  int state = STATE_AUTH;
  char user[MAXLINE+1], pass[MAXLINE+1], line[MAXLINE+1];
  char * userx, * passx;
  char * maildrop;
  int modified = 0;
  struct passwd * u_inf;
  struct group * g_inf;
#if HAVE_LIBSSL
  int rc;
  SSL_CTX * ctx = NULL;
  SSL * ssl = NULL;
  SSL_METHOD * meth = NULL;

  if (use_ssl()) {
    SSL_load_error_strings();
    SSLeay_add_ssl_algorithms();
    meth = SSLv23_server_method();
    ctx = SSL_CTX_new(meth);
    if (ctx==NULL) {
      syslog(LOG_ERR,"%s","SSL: failed to create new context");
      exit(EXIT_FAILURE);
    }

    if (SSL_CTX_use_certificate_file(ctx,ssl_certfile,SSL_FILETYPE_PEM) <= 0) {
      syslog(LOG_ERR,"%s: %s","SSL: failed to use certificate file",ssl_certfile);
      exit(EXIT_FAILURE);
    }

    if (SSL_CTX_use_PrivateKey_file(ctx,ssl_keyfile,SSL_FILETYPE_PEM) <= 0) {
      syslog(LOG_ERR,"%s: %s","SSL: failed to use key file",ssl_keyfile);
      exit(EXIT_FAILURE);
    }

    ssl = SSL_new(ctx);
    if (ssl==NULL) {
      syslog(LOG_ERR,"%s","SSL: failed to get new handle");
      exit(EXIT_FAILURE);
    }
    set_ssl_handle(ssl);

    SSL_set_fd(ssl,fd);
    rc = SSL_accept(ssl);
    if (rc==-1) {
      syslog(LOG_ERR,"%s","SSL: failed to accept connection");
      exit(EXIT_FAILURE);
    }
  }
#endif

  /* I'm ready */
  write_line(fd, "+OK\r\n");
begin_auth:

  /* first read username */
  read_line(fd,user,MAXLINE);
  if (strncasecmp(user,"QUIT",4)==0) {
    write_line(fd,"+OK\r\n");
    exit(EXIT_SUCCESS);
  }

  /* if input was not username, repeat this step */
  if (strncasecmp(user,"USER ",5)!=0) {
    write_line(fd,"-ERR log in with USER and PASS\r\n");
    goto begin_auth;
  }

  /* username is OK */
  write_line(fd,"+OK\r\n");

  /* now read password */
  read_line(fd,pass,MAXLINE);
  if (strncasecmp(pass,"QUIT",4)==0) {
    write_line(fd,"+OK\r\n");
    exit(EXIT_SUCCESS);
  }

  /* if input was not password, let user enter another
   * username and a password.
   */
  if (strncasecmp(pass,"PASS ",5)!=0) {
    write_line(fd,"-ERR log in with USER and PASS\r\n");
    read_line(fd,line,MAXLINE);
    while (strncasecmp(line,"PASS ", 5)!=0) {
      if (strncasecmp(line,"USER ",5)==0) {
        strlcpy(user,line,sizeof(user));
        write_line(fd,"+OK\r\n");
      } else if (strncasecmp(line,"QUIT ",5)==0) {
        write_line(fd,"+OK\r\n");
        exit(EXIT_SUCCESS);
      } else {
        write_line(fd,"-ERR log in with USER and PASS\r\n");
      }
      read_line(fd,line,MAXLINE);
    }
    strlcpy(pass,line,sizeof(pass));
  }

#define clean_up(str) { \
    char * x; \
    if ((x=index(str,'\r')) != NULL) { \
      *x = '\0'; \
    } else if ((x=index(str,'\n')) != NULL) { \
      *x = '\0'; \
    } \
  }

  /*
   * we both have username and password, we can now
   * authenticate.
   */
  if (authenticate(user+5,pass+5)==0) {
    write_line(fd,"-ERR authentication failed\r\n");
    clean_up(user);
    syslog(LOG_WARNING,"%s ('%.16s')","login failed",user+5);
    exit(EXIT_FAILURE);
  }

  clean_up(user);
  userx = user + 5;
  clean_up(pass);
  passx = pass + 5;

  /* we've reached transaction state */
  state = STATE_TRANS;

  if (real_username[0] != 0) {
    userx = real_username;
  }

  u_inf = getpwnam(userx);
  if (u_inf==NULL) {
    syslog(LOG_ERR,"%s: %s","UID lookup failed",strerror(errno));
    write_line(fd,"-ERR user no longer exists\r\n");
    exit(EXIT_FAILURE);
  }

  if (real_maildrop[0] != 0) {
    maildrop = real_maildrop;
  } else {
    if (local_mbox) {
      int md_len = strlen(u_inf->pw_dir)+strlen(local_mbox)+2;
      maildrop = alloca(md_len);
      if (maildrop) {
        snprintf(maildrop,md_len,"%s/%s",u_inf->pw_dir,local_mbox);
      } else {
        syslog(LOG_ERR,"%s: %s","alloca() failed",strerror(errno));
        write_line(fd,"-ERR unable to allocate enough memory\r\n");
        exit(EXIT_FAILURE);
      }
    } else {
      int md_len = strlen(mailspool)+strlen(userx)+1;
      maildrop = alloca(md_len);
      if (maildrop) {
        snprintf(maildrop,md_len,"%s%s",mailspool,userx);
      } else {
        syslog(LOG_ERR,"%s: %s","alloca() failed",strerror(errno));
      }
    }
  }

  mdl = maildrop;

  g_inf = getgrnam("mail");
  if (g_inf==NULL) {
    syslog(LOG_ERR,"%s","group 'mail' not found");
    write_line(fd,"-ERR group 'mail' not found\r\n");
    exit(EXIT_FAILURE);
  }
  if (setegid(g_inf->gr_gid)!=0 && real_username[0] == 0) {
    syslog(LOG_ERR,"%s: %d: %s","setegid() failed",g_inf->gr_gid,strerror(errno));
    write_line(fd,"-ERR failed to join 'mail' group\r\n");
    exit(EXIT_FAILURE);
  }

  if (lock_maildrop(maildrop,u_inf->pw_uid,g_inf->gr_gid)==0) {
    syslog(LOG_ERR,"%s: %s: %s","failed to lock maildrop",maildrop,strerror(errno));
    write_line(fd,"-ERR failed to lock maildrop\r\n");
    exit(EXIT_FAILURE);
  }

  if (seteuid(u_inf->pw_uid)!=0) {
    syslog(LOG_ERR,"%s: %d: %s","seteuid() failed",u_inf->pw_uid,strerror(errno));
    write_line(fd,"-ERR failed to assume user identity\r\n");
    exit(EXIT_FAILURE);
  }

  if (atexit(do_remove_lock)!=0) {
    syslog(LOG_WARNING,"%s: %s","atexit() failed; lock files may fail to expire",strerror(errno));
  }

  Signal(SIGPIPE,sig_handler);

  if (process_mails(maildrop)==0) {
    write_line(fd,"-ERR failed to scan maildrop contents\r\n");
    do_remove_lock();
    exit(EXIT_FAILURE);
  }

  /* authenticated and maildrop locked
   */
  write_line(fd,"+OK\r\n");

  read_line(fd,line,MAXLINE);
  while (strncasecmp(line,"QUIT",4)!=0) {
    if (strncasecmp(line,"STAT",4)==0) {
      long msg, size;
      get_stats(&msg,&size);
      snprintf(line,MAXLINE,"+OK %ld %ld\r\n",msg,size);
      write_line(fd,line);
    } else if (strncasecmp(line,"LIST",4)==0) {
      do_list(fd,line); /* parses itself */
    } else if (strncasecmp(line,"RETR ",5)==0) {
      retrieve_msg(fd,line); /* parses itself */
    } else if (strncasecmp(line,"DELE ",5)==0) {
      delete_msg(fd,line); /* parses itself */
      modified = 1;
    } else if (strncasecmp(line,"NOOP",4)==0) {
      write_line(fd,"+OK\r\n");
      /* do nothing */
    } else if (strncasecmp(line,"RSET",4)==0) {
      reset_msg();
      modified = 0;
      write_line(fd,"+OK\r\n");
    } else if (strncasecmp(line,"TOP ",4)==0) {
      show_top_msg(fd,line);
    } else if (strncasecmp(line,"UIDL",4)==0) {
      show_uidl(fd,line+4);
    } else {
      write_line(fd,"-ERR command not understood\r\n");
    }
    read_line(fd,line,MAXLINE);
  }

  write_line(fd,"+OK\r\n");
  close(fd);
#if HAVE_LIBSSL
  if (use_ssl()) {
    SSL_free(ssl);
    SSL_CTX_free(ctx);
  }
#endif

  if (modified) {
    do_update(maildrop);
  }
  remove_lock(maildrop);
}
