/*****************************************************************************

  POPular -- A POP3 server and proxy for large mail systems

  $Id: pcheckd.c,v 1.35 2003/11/22 21:27:22 sqrt Exp $

  http://www.remote.org/jochen/mail/popular/

******************************************************************************

  Copyright (C) 1999-2002  Jochen Topf <jochen@remote.org>

  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
  (at your option) 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

*****************************************************************************/

#if HAVE_CONFIG_H
# include <config.h>
#endif

#include "popular.h"
#include "daemon.h"


static char mbdir[MAXBUF];
static char maxsession_file[MAXBUF];
static int load_is_ok;

static long num_requests;
static long num_mailchecks;
static long num_mailchecks_old;
static long num_empty;
static long num_not_empty;
static long num_load_too_high;
static long num_maxsession_reached;

int debug;

int child_pid[MAX_PCHECKD_CHILDREN];


/*****************************************************************************

  handle_mailcheck()

*****************************************************************************/
int
handle_mailcheck(const char *mailbox, char *out)
{
  DIR *d;
  int n, len, i;
  char dirname[MAXLEN_DIRNAME];

  if (mailbox[0] == '/') {
    /* XLOG-DOC:ADM:0072:invalid_mailbox
     * The mailbox name that the client sent is invalid. The name is not
     * allowed to start with a '/' */
    xlog_printf(xlog_adm, 0x0072, "invalid_mailbox mailbox='%s'", mailbox);
    return strlcpy(out, "-ERR no mailbox", MAXLEN_MAILCHECK);
  }
  for (i=0; mailbox[i]; i++) {
    if ( !(isalnum((int)(mailbox[i])) ||
           mailbox[i] == '.' ||
           mailbox[i] == '_' ||
           mailbox[i] == '-' ||
           mailbox[i] == '+' ||
           mailbox[i] == '/' ||
           mailbox[i] == '%' ||
           mailbox[i] == '='
          ) ||
          (mailbox[i] == '.' && mailbox[i+1] == '.')
       ) {
      /* XLOG-DOC:ADM:0154:bad_mailbox
       * The mailbox name that the proxy sent to the pserv has funny
       * chars in it. Only the following chars are allowed: a-z, A-Z, 0-9,
       * '.', '_', '-', '+', '/', '=', '%'. Two adjacent dots ("..") are not
       * allowed. */
      xlog_printf(xlog_adm, 0x0154, "bad_mailbox mailbox='%s'", mailbox);
      return strlcpy(out, "-ERR no mailbox", MAXLEN_MAILCHECK);
    }
  }

  (void) strlcpy(dirname, mbdir,  sizeof(dirname));
  (void) strlcat(dirname, "/", sizeof(dirname));
  (void) strlcat(dirname, mailbox, sizeof(dirname));
  len = strlcat(dirname, "/new", sizeof(dirname));

  d = opendir(dirname);
  if (! d) {
    /* XLOG-DOC:ERR:0073:opendir_failed
     * The 'new' directory for the given mailbox can't be opened. This
     * probably means that the mailbox hasn't been created yet. */
    xlog_printf(xlog_err, 0x0073, "opendir_failed dir='%s' errno=%d errmsg='%s'", dirname, errno, strerror(errno));
    return strlcpy(out, "-ERR no mailbox", MAXLEN_MAILCHECK);
  }
  n=0;
  while (readdir(d)) {
    n++;
    if (n > 2) {
      closedir(d);
      num_not_empty++;
      return strlcpy(out, "+OK 2 new mail", MAXLEN_MAILCHECK);
    }
  }
  closedir(d);

  (void) strlcpy(dirname+len-3, "cur", 4);
  d = opendir(dirname);
  if (! d) {
    /* XLOG-DOC:ADM:0074:opendir_failed
     * The 'cur' directory for the given mailbox can't be opened. */
    xlog_printf(xlog_adm, 0x0074, "opendir_failed dir='%s' errno=%d errmsg='%s'", dirname, errno, strerror(errno));
    return strlcpy(out, "-ERR no mailbox", MAXLEN_MAILCHECK);
  }
  n=0;
  while (readdir(d)) {
    n++;
    if (n > 2) {
      closedir(d);
      num_not_empty++;
      return strlcpy(out, "+OK 1 mail", MAXLEN_MAILCHECK);
    }
  }
  closedir(d);

  num_empty++;
  return strlcpy(out, "+OK 0 no mail", MAXLEN_MAILCHECK);
}


/*****************************************************************************

  handle_loadcheck()

*****************************************************************************/
int
handle_loadcheck(const char *in, char *out)
{
  long load = get_load();

  if (load < 0) {
    (void) strlcpy(out, "-ERR not available", MAXLEN_MAILCHECK);
  } else {
    snprintf(out, MAXLEN_MAILCHECK, "+OK %ld", load);
  }

  return strlen(out);
}


/*****************************************************************************

  handle_request()

*****************************************************************************/
int
handle_request(const char *in, char *out)
{
  num_requests++;
  if (in[0] == '*') {
    if (in[1] == 'L') {
      return handle_loadcheck(in+2, out);
    } else if (in[1] == 'M') {
      if (load_is_ok) {
	struct stat sbuf;
	int sr = stat(maxsession_file, &sbuf);
	if (sr == -1) {
	  num_mailchecks++;
	  return handle_mailcheck(in+2, out);
	} else {
	  num_maxsession_reached++;
	  return strlcpy(out, "+OK 4 maxsession reached", MAXLEN_MAILCHECK);
	}
      } else {
	num_load_too_high++;
        return strlcpy(out, "+OK 3 load too high", MAXLEN_MAILCHECK);
      }
    } else {
      /* XLOG-DOC:ADM:0075:unknown_request
       * The client sent an unknown request. Allowed are 'L' for load check
       * and 'M' for mailbox check. */
      xlog_printf(xlog_adm, 0x0075, "unknown_request request='%s'", in);
      (void) strlcpy(out, "-ERR unknown request", MAXLEN_MAILCHECK);
      return strlen(out);
    }
  } else {		/* compatibility for old versions */
    num_mailchecks_old++;
    return handle_mailcheck(in, out);
  }
}


/*****************************************************************************

  pcheckd_reapchildren()

  Reap all deceased children, logging the event only if they died an
  unnatural death.

*****************************************************************************/
void
pcheckd_reapchildren()
{
  pid_t pid;
  int status, i;

  while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
    if (WIFEXITED(status) && WEXITSTATUS(status)) {
      /* XLOG-DOC:ADM:0156:child_died
       * A child of pcheckd died with non-zero exit code. See the log entries
       * of the child for more details about what happened. */
      xlog_printf(xlog_adm, 0x0156, "child_died pid=%d status=%d", pid, WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
      /* XLOG-DOC:ADM:0157:child_died_signal
       * A child of pcheckd died because of a signal. This can either mean
       * that there is a bug in the program of the system has big problems
       * (like running out of memory). */
      xlog_printf(xlog_sos, 0x0157, "child_died_signal pid=%d signal=%d", pid, WTERMSIG(status));
    }
    DEBUG1(DG_MAIN, "reapchildren", "child died pid=%d", pid);
    for (i=0; i<MAX_PCHECKD_CHILDREN; i++) {
      if (child_pid[i] == pid) {
	child_pid[i] = 0;
	break;
      }
    }
  }
}


/*****************************************************************************

  print_usage()

*****************************************************************************/
void
print_usage(const char *prg)
{
  printf("Usage: %s [OPTION] ...\n", prg);
  printf("Daemon that can be asked to check for mails in mailboxes.\n");
  printf("Options:\n");
  printf("  -l, --logfile=FILE     set logfile name (default: `%s')\n", PCHECKD_LOG_FILE);
  printf("  -m, --mailboxdir=DIR   set mailbox directory (default: `%s')\n", POPDIR);
  printf("  -p, --port=PORT        set UDP port to listen on (default: %d)\n", DEFAULT_PORT_CHECK);
  printf("  -e, --emptyload=LOAD   if load is higher than this, all mailboxes\n");
  printf("                         are empty (0=off, default: 0)\n");
  printf("  -r, --rundir=DIR       set name of directory where pidfile is\n");
  printf("                         saved (default: `%s')\n", RUN_DIR);
  printf("  -f, --fork=NUM         number of children to fork\n");
  printf("                         (default: no fork, max: %d)\n", MAX_PCHECKD_CHILDREN);
  printf("  -d, --debug            enable debugging messages\n");
  printf("  -h, --host=HOST        host name or IP number to bind to\n");
  printf("                         (default: INADDR_ANY)\n");
  printf("      --nodaemon         don't start as daemon\n");
  printf("      --help             print this help message\n");
  printf("      --version          print version information\n");
  printf("\n");
}


/*****************************************************************************

  main()

*****************************************************************************/
int
main(int argc, char *argv[])
{
  int s;
  int c, i, daemon=1, checkport=DEFAULT_PORT_CHECK;
  int emptyload=0;
  char host[80] = "";
  const int one = 1;
  struct sockaddr_in sa;
  struct sockaddr remote;
  unsigned int remotelen = sizeof(remote);
  char inmsg[MAXLEN_MAILCHECK+1];
  char outmsg[MAXLEN_MAILCHECK+1];
  char *logfile = PCHECKD_LOG_FILE;
  char rundir[MAXBUF] = RUN_DIR;
  char *prgname;
  int num_fork=0;
  int parent=1;

  static struct option long_options[] = {
    { "help",          no_argument,       0, 0 },
    { "version",       no_argument,       0, 0 },
    { "debug",         no_argument,       0, 'd' },
    { "nodaemon",      no_argument,       0, 0 },
    { "rundir",        required_argument, 0, 'r' },
    { "port",          required_argument, 0, 'p' },
    { "logfile",       required_argument, 0, 'l' },
    { "mailboxdir",    required_argument, 0, 'm' },
    { "emptyload",     required_argument, 0, 'e' },
    { "fork",          required_argument, 0, 'f' },
    { "host",          required_argument, 0, 'h' },
    { NULL,            0,                 0, 0 }
  };

  prgname = argv[0];            /* program name for error messages */

  strlcpy(mbdir, POPDIR, sizeof(mbdir));

  debug = 0;

  for (i=0; i<MAX_PCHECKD_CHILDREN; i++) child_pid[i] = 0;


  /***************************************************************************
    Parse command line options.
  ***************************************************************************/
  while (1) {
    int option_index = 0;
    c = getopt_long(argc, argv, "l:m:p:e:r:f:h:d", long_options, &option_index);

    if (c == -1) break;

    switch (c) {
      case 0:
        if (! strcmp(long_options[option_index].name, "version")) {
          printf("pcheckd - POPular POP server suite %s\n", VERSION);
          exit(RCODE_OK);
        } else if (! strcmp(long_options[option_index].name, "help")) {
          print_usage(prgname);
          exit(RCODE_OK);
        } else if (! strcmp(long_options[option_index].name, "nodaemon")) {
	  daemon = 0;
        } else {
          fprintf(stderr, "%s: unknown option: %s\n", prgname, long_options[option_index].name);
          exit(RCODE_INTERNAL);
        }
        break;
      case 'l':		/* log file */
	logfile = optarg;
        break;
      case 'm':		/* mailboxdir */
        strlcpy(mbdir, optarg, sizeof(mbdir));
        break;
      case 'd':		/* debug */
	debug = DG_ALL;
        break;
      case 'p':		/* UDP port */
        checkport = get_port(optarg);
        if (checkport < 0) {
          fprintf(stderr, "%s: port must be valid UDP port.\n", prgname);
          exit(RCODE_CMDLINE);
        }
        break;
      case 'f':		/* fork */
	num_fork = get_int(optarg, 0, MAX_PCHECKD_CHILDREN, -1, -1, -1);
	if (num_fork < 0) {
          fprintf(stderr, "%s: fork argument must be between 0 and %d.\n", prgname, MAX_PCHECKD_CHILDREN);
          exit(RCODE_CMDLINE);
	}
	break;
      case 'r':		/* rundir */
	strlcpy(rundir, optarg, sizeof(rundir));
	break;
      case 'h':		/* host or IP number */
	strlcpy(host, optarg, sizeof(host));
	break;
      case 'e':		/* emptyload */
	emptyload = get_int(optarg, 0, 9999, -1, -1, -1);
	if (emptyload < 0) {
          fprintf(stderr, "%s: emptyload must be 0 (=off) or between 1 and 9999.\n", prgname);
          exit(RCODE_CMDLINE);
	}
        break;
      default:
        fprintf(stderr, "Try `%s --help' for more information.\n", prgname);
        exit(RCODE_CMDLINE);
    }
  }

  if (optind != argc) {
    fprintf(stderr, "%s: invalid extra arguments.\nTry `%s --help' for more information.\n", prgname, prgname);
    exit(RCODE_CMDLINE);
  }

  if (access(mbdir, R_OK) != 0) {
    fprintf(stderr, "%s: access to `%s' failed: %s\n", prgname, mbdir, strerror(errno));
    exit(RCODE_ERR);
  }

  strlcpy(maxsession_file, rundir, sizeof(maxsession_file));
  strlcat(maxsession_file, "/", sizeof(maxsession_file));
  strlcat(maxsession_file, PSERV_MAX_FILE, sizeof(maxsession_file));


  /***************************************************************************
    Open socket and bind to port.
  ***************************************************************************/
  if ((s = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    fprintf(stderr, "%s: socket error: %s\n", prgname, strerror(errno));
    exit(RCODE_ERR);
  }

  if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)) != 0) {
    fprintf(stderr, "%s: setsockopt failed: %s\n", prgname, strerror(errno));
    exit(RCODE_ERR);
  }
 
  memset(&sa, 0, sizeof(sa));
  sa.sin_family      = AF_INET;
  sa.sin_port        = htons(checkport);

  if (host[0]) {
    struct hostent *hostent;
    hostent = gethostbyname(host);
    if (hostent == NULL || (hostent->h_addr_list)[0] == NULL) {
      fprintf(stderr, "%s: unkown host or IP: %s\n", prgname, host);
      exit(RCODE_ERR);
    }
    memcpy(&(sa.sin_addr.s_addr), hostent->h_addr_list[0], sizeof(sa.sin_addr.s_addr));
  } else {
    sa.sin_addr.s_addr = htonl(INADDR_ANY);
  }

  if (bind(s, (struct sockaddr *) &sa, sizeof(sa)) < 0) {
    fprintf(stderr, "%s: bind failed: %s\n", prgname, strerror(errno));
    exit(RCODE_ERR);
  }


  /***************************************************************************
    Daemonize unless --nodaemon option was given.
  ***************************************************************************/
  if (daemon) daemonize(prgname);


  /***************************************************************************
    Open log file.
  ***************************************************************************/
  if (xlog_open(PCHECKD_PRG_NAME, logfile, PCHECKD_LOG_MODE) < 0) {
    fprintf(stderr, "%s: couldn't open logfile '%s': %s\n", prgname, logfile, strerror(errno));
    exit(RCODE_ERR);
  }
  /* XLOG-DOC:INF:0070:start
   * Pcheckd has been started and partially initialized. */
  xlog_printf(xlog_inf, 0x0070, "start version='%s' port=%d mailboxdir='%s' emptyload=%d", VERSION, checkport, mbdir, emptyload);


  /***************************************************************************
    Open pid file.
  ***************************************************************************/
  write_pidfile(rundir, PCHECKD_PID_TEMPL, PCHECKD_PID_FILE);


  /***************************************************************************
    Setup signal handling.
  ***************************************************************************/
  signal_init(1);

  if (num_fork) {
    int i;
    for (i=0; i<num_fork; i++) {
      int pid = fork();
      if (pid < 0) {
	/* XLOG-DOC:SOS:0155:fork_failed
	 * The fork () system call in pcheckd failed. */
	xlog_printf(xlog_sos, 0x0155, "fork_failed errno=%d errmsg='%s'", errno, strerror(errno));
      }
      if (pid == 0) {
	parent = 0;
	break;
      }
      child_pid[i] = pid;
    }
  }


  /***************************************************************************
    Event loop.
  ***************************************************************************/
  load_is_ok = 1;
  while (1) {
    int n, sig, r;
    fd_set readset;

    FD_ZERO(&readset);
    FD_SET(s, &readset);
    r = rel_select(&readset, 1, 0);

    load_is_ok = load_ok(emptyload);

    if (r < 0) {
      /* XLOG-DOC:SOS:0077:select_error
       * The select () syscall in the main loop returned an error that is
       * not handled. This should never happen. */
      xlog_printf(xlog_sos, 0x0077, "select_error errno=%d errmsg='%s'", errno, strerror(errno));
      continue;
    }

    while ((sig = signal_next())) {
      switch (sig) {
	case SIGCHLD:
	  pcheckd_reapchildren();
	  break;
	case SIGQUIT: /* fallthrough */
	case SIGINT:  /* fallthrough */
	case SIGTERM: /* fallthrough */
	  /* XLOG-DOC:INF:0078:shutdown
	   * pcheckd received a QUIT, INT, TER, or PWR signal and is
	   * shutting down. */
	  xlog_printf(xlog_inf, 0x0078, "shutdown");
	  if (parent) {
	    unlink_in_rundir(rundir, PCHECKD_PID_TEMPL, PCHECKD_PID_FILE);
	    for (i=0; i<MAX_PCHECKD_CHILDREN; i++) {
	      if (child_pid[i] > 0) kill(child_pid[i], SIGTERM);
	    }
	  }
	  exit(RCODE_OK);
	case SIGHUP:
	  /* XLOG-DOC:INF:0144:stat
	   * Statistics from pcheckd: 'requests' is the number of all
	   * requests that pcheckd got, 'mailchecks' is the number of
	   * new format mailcheck requests, 'mailchecks_old' the number
	   * of old format mailcheck requests, 'empty' the number of
	   * requests finding an empty mailbox, 'not_empty' the number of
	   * requests finding a not empty mailbox, 'load_too_high'
	   * the number of requests answered with that message, and
	   * 'maxsession_reached' the number of messages send while the
	   * maximum number of pserv sessions was used. The
	   * statistics are logged and cleared on SIGHUP. */
	  xlog_printf(xlog_inf, 0x0144, "stat requests=%ld mailchecks=%ld mailchecks_old=%ld empty=%ld not_empty=%ld load_too_high=%ld maxsession_reached=%ld", num_requests, num_mailchecks, num_mailchecks_old, num_empty, num_not_empty, num_load_too_high, num_maxsession_reached);
	  num_requests           = 0;
	  num_mailchecks         = 0;
	  num_mailchecks_old     = 0;
	  num_empty              = 0;
	  num_not_empty          = 0;
	  num_load_too_high      = 0;
	  num_maxsession_reached = 0;
	  xlog_reopen(logfile, PCHECKD_LOG_MODE);
	  break;
	case SIGUSR1:
	  /* XLOG-DOC:INF:0079:debug_disabled
	   * pcheckd received a SIGUSR1 and disabled debuglogging. */
	  xlog_printf(xlog_inf, 0x0079, "debug_disabled");
	  debug=0;
	  break;
	case SIGUSR2:
	  /* XLOG-DOC:INF:007a:debug_enabled
	   * pcheckd received a SIGUSR2 and enabled debuglogging. */
	  xlog_printf(xlog_inf, 0x007a, "debug_enabled");
	  debug=1;
	  break;
	default: /* ignore other signals */
	  break;
      }
    }

    if (r == 0) continue;

    n = recvfrom(s, inmsg, MAXLEN_MAILCHECK, 0, &remote, &remotelen);

    if (n >= 0) {
      inmsg[n] = '\0';
      if (n>0 && inmsg[n-1] == '\n') inmsg[--n] = '\0';
      if (n>0 && inmsg[n-1] == '\r') inmsg[--n] = '\0';

      DEBUG1(DG_MAIN, "main", "recv_msg msg='%s'", inmsg);

      if ((n = handle_request(inmsg, outmsg)) > 0) {
	DEBUG1(DG_MAIN, "main", "send_msg msg='%s'", outmsg);
	(void) strlcat(outmsg, "\n", sizeof(outmsg));
	if (sendto(s, outmsg, n+1, 0, &remote, remotelen) != n+1) {
	  /* XLOG-DOC:ERR:0076:sendto_failed
	   * Sending the answer of the requested check back to the client
	   * failed. */
	  xlog_printf(xlog_err, 0x0076, "sendto_failed errno=%d errmsg='%s'", errno, strerror(errno));
	}
      }
    }
  }
}


/** THE END *****************************************************************/


syntax highlighted by Code2HTML, v. 0.9.1