/* mdpop3d.c
   POP3 daemon for MailDirs.

   Copyright (C) 2000, 2001 Michael Tokarev.

   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.

   The author of the program may be contacted at mjt@corpit.ru
*/

/* to compile with PAM support, #define USE_PAM. */
/* to compile with APOP command support, #define USE_APOP (PAM required) */

/* todo:
 Server-side (forced or per-user) message expiration (maybe)
 AUTH command (rfc1734, rfc2195) support?
 RESP-CODES (rfc2449) support?
 LOGIN-DELAY (rfc2449) support?
 Resolving of remote ip address, with verify (maybe)
 Chroot to maildir for safety (not clean task, requires switching uids
  from/to root)
 Reporting of error conditions (like OOM, unable to open file etc) (maybe)
 Store message size *in* maildir filename, like e.g. courier does?
 Submailboxes support ala courier? (with username+folder)
 Provide an example of pam_apop module?
 STLS command? (requires me to understand how e.g. sasl works and if this can
  be done via PAM w/o the whole sasl thing -- using openssl for encryption and
  PAM for auth)
 More efficient maildir.  This is to say -- do not use maildir format, but
  other similar format with one message per file, all in one dir, with possible
  hashing.  This needs to be syncronized with other systems that use this mail
  storage...
 Look more closely to order of PAM calls.  According to last messages in
  Linux-PAM mailinglist, something should be wrong with pam_setcred() calling
  order.  The evil thing with PAM is that it does nasty things with syslog,
  and that's a real PITA to deal with...
*/

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <pwd.h>
#include <grp.h>
#include <syslog.h>
#include <errno.h>
#include <stdlib.h>
#include <dirent.h>
#ifdef USE_PAM
#include <security/pam_appl.h>
#else
#ifdef USE_APOP
# error APOP requires PAM
#endif
#include <pwd.h>
#include <unistd.h>
#endif

static const char *rhost; /* = NULL; remote host/addr for logging */
static const char *user; /* = NULL; username */

static int timeout = 60; /* general i/o timeout */
static const char *maildir; /* = NULL; name of maildir */
static int debug; /* = 0; debug flag */
static int commit_on_err; /* = 0; if true, issue QUIT on comm error */
static int nomove; /* = 0; do not move messages to cur/ */

static const char *const version = "mdpop3 " VERSION;
static const char *const maildirenv = "MAILDIR"; /* name of Maildir env. var */

static char line[1024]; /* this is main buffer; used for output also */
static int llen; /* = 0; for buffered writes */

#define OK "+OK "
#define ERR "-ERR "

#ifdef __GNUC__
static void putline(const char *fmt, ...) __attribute__((format(printf,1,2)));
static void putmline(const char *fmt, ...) __attribute__((format(printf,1,2)));
static void die(const char *why) __attribute__((noreturn));
#else
static void die(const char *why);
#endif

/* write to client with timeout */
static void twrite(const char *b, int l) {
  alarm(timeout);
  if (write(1, b, l) != l)
    die("client write error");
  alarm(0);
}

/* put a line to client.  Note that this routine uses line[] buffer */
static void putline(const char *fmt, ...) {
  int l;
  va_list ap;
  va_start(ap, fmt);
  l = vsnprintf(line, sizeof(line) - 2, fmt, ap);
  va_end(ap);
  if (debug) syslog(LOG_DEBUG, "out: %s", line);
  line[l++] = '\r'; line[l++] = '\n';
  twrite(line, l);
}

/* buffered client writes.
  flushmline() flushes the buffer
  putmline() similar to putline() but adds to buffer
  line[] used for buffering!
 */

static void flushmline() {
  if (llen) {
    twrite(line, llen);
    llen = 0;
  }
}

static void putmline(const char *fmt, ...) {
  int l;
  for(;;) {
    if (llen < sizeof(line) - 3) {
      va_list ap;
      va_start(ap, fmt);
      l = vsnprintf(line + llen, sizeof(line) - 2 - llen, fmt, ap);
      va_end(ap);
      if (l > 0 && l + llen < sizeof(line) - 2)
        break;
    }
    /* not enouth space */
    flushmline();
  }
  if (debug) syslog(LOG_DEBUG, "out: %s", line + llen);
  llen += l;
  line[llen++] = '\r'; line[llen++] = '\n';
}

static void ok() { putline("+OK"); }
static void needarg() { putline(ERR "argument expected"); }
static void extraarg() { putline(ERR "unexpected argument"); }
static void needauth() { putline(ERR "user/pass first"); }

static struct msg {
  int flag;
  off_t size;
  time_t time;
  char fn[1]; /* varstr */
} **msgp; /* = NULL; array of message pointers */
static int nmsg; /* = 0; total number of messages */
static int amsg; /* = 0; allocated size of *msgp (count of msgs) */
static int cmsg; /* = 0; current number of messages (excluding deleted) */
static off_t msgsz; /* = 0; current size of undeleted messages */
static int o_nmsg; /* = 0; original number of messages */
static off_t o_msgsz; /* = 0; original size of all messages */

#define F_DELETED 0x01
#define F_NEW     0x02
#define F_SEEN    0x04

static void nomem(int size) { /* report OOM condition */
  syslog(LOG_ERR, "unable to allocate %d bytes of memory", size);
}

int addmsg(const char *fn, off_t size, time_t time, int flag) {

/* addmsg is only one place where we'll allocate memory.
 Since pop3d will never free message structures memory, we use
 a trivial wrapper around malloc that allocate large chunks of
 memory at once and fill it up with message structs, allocating
 new block when current one will be filled.
 */
  static char *m_bptr; /* = NULL; pointer to last allocated free space */
  static int m_bsize;  /* = 0; size available on last block */
#ifndef MCSIZE
# define MCSIZE 10240 /* size of one block */
#endif

  int a, b, i;
  struct msg *m;

  /* allocate memory first */
  i = sizeof(struct msg) + strlen(fn); /* size of block to alloc */
  if (m_bsize < i) { /* last block does not have needed space */
    char *p = (char*)malloc(MCSIZE);
    if (!p) {
      nomem(MCSIZE);
      return -1;
    }
    m_bptr = p;
    m_bsize = MCSIZE;
  }
  m = (struct msg *)m_bptr;

  /* round i to sizeof(int) */
  if ((a = i % sizeof(int)) != 0)
    i += sizeof(int) - a;
  m_bptr += i;
  m_bsize -= i; /* it's Ok but unlikely to have m_bsize < 0 here */

  /* init message struct */
  m->flag = flag; m->size = size; m->time = time;
  strcpy(m->fn, fn);

  if (nmsg == amsg) { /* need to (re)allocate pointers */
    struct msg **nm;
    if (!msgp)
      /* allocate 128 message entries initially */
      nm = (struct msg **)malloc((i = 128) * sizeof(struct msg *));
    else
      /* double allocated message count each time */
      nm = (struct msg **)realloc(msgp, (i = nmsg * 2) * sizeof(struct msg *));
    if (!nm) {
      nomem(i * sizeof(struct msg *));
      /* "return" last allocated message buffer back */
      m_bsize += (m_bptr - (char*)m);
      m_bptr = (char*)m;
      return -1;
    }
    msgp = nm;
    amsg = i;
  }

  /* find a place for new message, sorting by time */
  a = 0; b = nmsg - 1;
  while(a <= b) {
    i = (a + b) >> 1;
    if (msgp[i]->time < time)
      a = i + 1;
    else if (msgp[i]->time > time)
      b = i - 1;
    else {
      a = i;
      break;
    }
  }

  /* insert new message */
  if (a < nmsg)
    memmove(msgp + a + 1, msgp + a, (nmsg - a) * sizeof(struct msg *));
  msgp[a] = m;
  ++nmsg; ++cmsg; msgsz += size;

  return a;
}

static void info() {
  putline(OK "maildir: %d message(s) %ld octet(s)", cmsg, (long int)msgsz);
}

static char *msgfn(int n) {
  if (strlen(msgp[n]->fn) >= sizeof(line) - 4)
    return NULL;
  strcpy(line, msgp[n]->flag & F_NEW ? "new/" : "cur/");
  strcpy(line + 4, msgp[n]->fn);
  return line;
}

static void initmaildir() {
  DIR *dir;
  struct dirent *de;
  struct stat st;
  int f;

  if (!maildir && (maildir = getenv(maildirenv)) == NULL)
    maildir = "Maildir";

  if (chdir(maildir) != 0) {
    /* assume no mail if no maildir */
    info();
    return;
  }

  /* cleanup tmp dir */
  if ((dir = opendir("tmp")) != NULL) {
    time_t now = time(NULL);
    strcpy(line, "tmp/");
    while((de = readdir(dir)) != NULL) {
      if (de->d_name[0] != '.' &&
	  strlen(de->d_name) < sizeof(line) - 4 &&
	  stat((strcpy(line+4, de->d_name), line), &st) == 0 &&
	  now - 36*60*60 > st.st_atime)
	unlink(line);
    }
    closedir(dir);
  }

  /* scan new and cur */
  /* we're totally quiet about errors here, just skipping them */
  f = F_NEW;
  strcpy(line, "new/");
  for(;;) {
    if ((dir = opendir(line)) != NULL) {
      while((de = readdir(dir)) != NULL) {
	if (de->d_name[0] != '.' &&
	    strlen(de->d_name) < sizeof(line) - 4 &&
	    stat((strcpy(line+4, de->d_name), line), &st) == 0 &&
	    S_ISREG(st.st_mode))
	  addmsg(de->d_name, st.st_size, st.st_mtime, f);
      }
      closedir(dir);
    }
    if (!f)
      break;
    f = 0;
    strcpy(line, "cur/");
  }

  o_msgsz = msgsz;
  o_nmsg = nmsg;
  info();
}

#define doopenlog() openlog("mdpop3d", LOG_PID, LOG_MAIL)

#ifdef USE_APOP
static char *apoptag;
# define IFAPOP(x) x
#else
# define IFAPOP(x)
#endif

#ifndef USE_PAM
# define IFPAM(x)
#else
#define IFPAM(x) x

static pam_handle_t *pamh;
static const char *pam_service = "pop3";

struct popdata {
  const char *pass;
  char *msg;
};

static int popd_conv(int nmsg, const struct pam_message **msgp,
		     struct pam_response **respp, void *appdata) {

  int i;
  struct popdata *d = (struct popdata *) appdata;
  struct pam_response *resp =
    (struct pam_response *) malloc(sizeof(struct pam_response) * nmsg);

  if (!resp)
    return PAM_BUF_ERR;

  for (i = 0; i < nmsg; i++) {
    resp[i].resp_retcode = PAM_SUCCESS;
    switch (msgp[i]->msg_style) {
    case PAM_PROMPT_ECHO_ON:
      resp[i].resp = strdup(user);
      break;
    case PAM_PROMPT_ECHO_OFF:
      resp[i].resp = d && d->pass ? strdup(d->pass) : NULL;
      break;
    case PAM_ERROR_MSG:
      if (msgp[i]->msg && d && !d->msg)
	d->msg = strdup(msgp[i]->msg);
      /* fall through */
    case PAM_TEXT_INFO:
      /* ignore it, but pam still wants a NULL response... */
      resp[i].resp = NULL;
      break;
    default:
      /* Must be an error of some sort... */
      free(resp);
      return PAM_CONV_ERR;
    }
  }
  *respp = resp;
  return PAM_SUCCESS;
}

static struct pam_conv convstruct = { popd_conv, NULL }; 

#endif

static struct passwd *checkpass(char *pass) {
  struct passwd *pw;

#ifdef USE_PAM


  int r;
  struct popdata d;
  const char *u = NULL;
  const char **ptr_u = &u;
  
  d.pass = pass;
  d.msg = NULL;
  convstruct.appdata_ptr = &d;

#define PAMOK(x) ((r = x) == PAM_SUCCESS) /* shortcut */
  /* ok, do all the PAM work.  I don't use PAM_SILENT flag since mdpop3d
     is able to display at least one pam message. */
  if (!PAMOK(pam_start(pam_service, user, &convstruct, &pamh))) {
    syslog(LOG_ERR, "[%s] unable to init PAM: %s",
	   rhost, pam_strerror(pamh,r));
    pw = NULL;
  }
  else
    if (PAMOK(pam_start(pam_service, user, &convstruct, &pamh)) &&
	PAMOK(pam_set_item(pamh, PAM_USER, user)) &&
	PAMOK(pam_set_item(pamh, PAM_RHOST, rhost)) &&
	PAMOK(pam_authenticate(pamh, 0)) &&
	PAMOK(pam_acct_mgmt(pamh, 0)) &&
	PAMOK(pam_get_item(pamh, PAM_USER, (const void **)ptr_u))) {
    pw = getpwnam(u && *u ? u : user); /* use username from pam if any */
    if (!maildir) /* try to get MAILDIR from PAM */
      maildir = pam_getenv(pamh, maildirenv);
  }
  else
    pw = NULL;
#undef PAMOK
  doopenlog(); /* reopen log since PAM munges it */
  convstruct.appdata_ptr = NULL; /* do not attempt to use this anymore */
  if (pw) {
    if (d.msg)
      free(d.msg);
    return pw;
  }
  syslog(LOG_INFO, "[%s] login incorrect for [%s]: %s", rhost, user,
	 pam_strerror(pamh, r));
  pam_end(pamh, r == PAM_SUCCESS ? PAM_USER_UNKNOWN : r);
  pamh = NULL;
  sleep(3);
  if (d.msg) {
    char *p, *n;
    /* convert multiline message to single line */
    for(p = d.msg; (n = strchr(p, '\n')) != NULL; p = n + 1)
      *n = ' ';
    /* it is safe to use long response -- we use snprintf anyway. */
    putline(ERR "login incorrect: %s", d.msg);
    free(d.msg);
  }
  else
    putline(ERR "login incorrect");
  return NULL;

#else /* !USE_PAM */
    
  pw = getpwnam(user);
  endpwent();
  if (pw != NULL)
   {
    if (pw->pw_passwd && strcmp(crypt(pass, pw->pw_passwd), pw->pw_passwd) == 0)
      return pw;
    syslog(LOG_INFO, "[%s] login incorrect for [%s]", rhost, user);
  }
  else
    syslog(LOG_INFO, "[%s] user unknown: [%s]", rhost, user);
  putline(ERR "login incorrect");
  return NULL;

#endif /* USE_PAM */
}

static int auth(char *pass) {
  struct passwd *pw = checkpass(pass);

  memset(line, 0, sizeof(line)); /* clear passwd */

  if (!pw)
    return -1;

  if (initgroups(user, pw->pw_gid) != 0 ||
      setgid(pw->pw_gid) != 0 ||
      setuid(pw->pw_uid) != 0) {
    syslog(LOG_ERR, "[%s][%s] unable to set privileges: %m", rhost, user);
    putline(ERR "unable to setup privileges");
    return -1;
  }
  if (chdir(pw->pw_dir) != 0) {
    syslog(LOG_ERR, "unable to access home directory %s: %m", pw->pw_dir);
    putline(ERR "unable to access homedir");
    return -1;
  }

#ifdef USE_PAM
  {
    int r = pam_open_session(pamh, PAM_SILENT);
    doopenlog(); /* reopen log since PAM munges it */
    if (r != PAM_SUCCESS) {
      syslog(LOG_ERR, "[%s][%s]: unable to open session: %s",
	     rhost, user, pam_strerror(pamh, r));
      putline(ERR "unable to open session");
      pam_end(pamh, r);
      pamh = NULL;
      return -1;
    }
  }
#endif

  initmaildir();

  return 0;
}

static int getnum(char **arg) {
  char *a = *arg;
  int n = 0;
  if (!a)
    needarg();
  else {
    while (*a >= '0' && *a <= '9')
      if ((n = n * 10 + (*a++ - '0')) < 0) {
	putline(ERR "invalid number");
	return -1;
      }
    if (a == *arg)
      putline(ERR "number expected");
    else if (*a != 0 && *a != ' ')
      putline(ERR "syntax error");
    else {
      while(*a == ' ')
	++a;
      *arg = *a ? a : NULL;
      return n;
    }
  }
  return -1;
}

static int getmsgno(char **arg, int expectarg) {
  int n = getnum(arg);
  if (n < 0)
    ;
  else if (!n || n > nmsg)
    putline(ERR "no such message");
  else if (msgp[--n]->flag & F_DELETED)
    putline(ERR "message deleted");
  else if (!expectarg && *arg)
    extraarg();
  else if (expectarg > 0 && !*arg)
    needarg();
  else
    return n;
  return -1;
}

static void dolist(int n, int uidl, const char *code) {
  if (uidl) {
    char *s = strchr(msgp[n]->fn + 1, ':');
    if (s)
      *s = '\0';
    putmline("%s%d %s", code, n+1, msgp[n]->fn);
    if (s)
      *s = ':';
  }
  else
    putmline("%s%d %ld", code, n+1, (long int)msgp[n]->size);
}

static void finalupdate() {
  int i;
  if (cmsg && !nomove)
    mkdir("cur", 0700);
  for (i = 0; i < nmsg; ++i) {
    if (msgp[i]->flag & F_DELETED)
      unlink(msgfn(i));
    else if ((msgp[i]->flag & F_NEW) && !nomove) {
      char fn[sizeof(line)+2]; /* +2 bytes for ":2" */
      strcpy(fn, "cur/"); strcpy(fn+4, msgp[i]->fn); /* will fit */
      if (strchr(fn + 5, ':') == NULL)
	strcat(fn + 5, ":2");
      rename(msgfn(i), fn);
    }
  }
  msgp = NULL;
}

static void putmsg(FILE *f, int lim) {
  int inh = 1, l = 0, c, s = 0;
  ok(); /* one packet for OK, and try as little packets as possible for rest */
  if (debug) syslog(LOG_DEBUG, "out: <message contents>");
  while((c = getc(f)) != EOF) {
    if (c == '\r') /* strip CRs from input completely */
      continue;
    if (s > sizeof(line) - 7) { /* reserve 7 chars for termination */
      twrite(line, s);
      s = 0;
    }
    if (c == '\n') {
      line[s++] = '\r'; line[s++] = '\n';
      if (!l) inh = 0;
      else l = 0;
      if (lim >= 0 && !inh && !lim--)
	break;
    }
    else {
      if (!l++ && c == '.')
	line[s++] = '.';
      line[s++] = c;
    }
  }
  if (ferror(f))
     syslog(LOG_ERR, "[%s][%s] error reading message file: %m",
            rhost, user);
  if (l) { line[s++] = '\r'; line[s++] = '\n'; }
  line[s++] = '.'; line[s++] = '\r'; line[s++] = '\n';
  twrite(line, s);
  if (debug) syslog(LOG_DEBUG, "out: .");
}

static void finallog(const char *why) {
  syslog(LOG_INFO, "[%s][%s] %s (%d/%ld msgs/bytes left, %d/%ld deleted)",
         rhost, user, why, cmsg, (long int)msgsz, o_nmsg - cmsg, (long int)o_msgsz - (long int)msgsz);
}

static void die(const char *why) {
  if (commit_on_err && msgp && nmsg != cmsg) {
    finallog(why);
    finalupdate();
  }
#ifdef USE_PAM
  if (pamh) {
    pam_close_session(pamh, PAM_SILENT);
    pam_end(pamh, PAM_SUCCESS); /* XXX: Is PAM_SUCCESS appropriate here? */
  }
#endif
  exit(0);
}

static void sig(int sig) {
  const char *s;
  switch(sig) {
    case SIGALRM: s = "timeout"; break;
    case SIGPIPE: s = "pipe closed"; break;
    case SIGHUP: s = "hungup"; break;
    default: s = "got signal"; break;
  }
  syslog(LOG_ERR, "[%s] %s, exiting", rhost, s);
  die(s);
}

int main(int argc, char **argv) {

  char *a;
  int i;
  int junk = 50;		/* max number of junk commands */
  int authenticated = 0;
  int quiet = 0;
  char *rhostvar = NULL;

  while((i = getopt(argc, argv,
		    "t:cau:r:m:qndR:Vh" IFPAM("s:") IFAPOP("AT:"))) != EOF)
    switch(i) {
    case 'r': rhost = optarg; break;
    case 'R': rhostvar = optarg; break;
    case 'u': user = optarg; authenticated = 1; break;
    case 't':
      if ((timeout = atoi(optarg)) <= 0) timeout = 20;
      break;
    case 'c': commit_on_err = 1; break;
    case 'a': authenticated = 1; break;
    case 'm': maildir = optarg; break;
    case 'q': quiet = 1; break;
    case 'n': nomove = 1; break;
    case 'd': debug = 1; break;
    IFPAM(case 's': pam_service = optarg; break;)
    IFAPOP(case 'A': if (!apoptag) apoptag = (char*)-1; break;)
    IFAPOP(case 'T': apoptag = optarg; break;)
    case 'V':
    case 'h':
      printf("%s: Maildir POP3 daemon %s" IFPAM(" PAM") IFAPOP(" APOP") "\n\
Written by Michael Tokarev <mjt@corpit.ru>\n", argv[0], version);
      if (i == 'h')
	puts("Options:\n\
 -h - print this help and exit\n\
 -V - print version info and exit\n\
 -a - go to pre-auth mode (TRANSACTION state)\n\
 -u user - pre-auth with username\n\
 -r rhost - set remote host name/addr\n\
 -R rhostvar - use ${rhostvar} as remote host name/addr\n\
 -t tmout - set command timeout in secs\n\
 -c - commit maildir on error - on communication errors,\n\
  update maildir just like on QUIT command\n\
  Note that this violates POP3 but sometimes helps with broken\n\
  mail clients on slow dialup links.\n\
 -q - be somewhat syslog-quiet\n\
 -d - debug mode (passwords will be logged!)\n\
 -n - do not move messages to cur/\n\
 -m maildir - cd to this maildir directory\n"
IFPAM(" -s service - use this PAM service\n")
IFAPOP(" -A - enable APOP command with default host tag\n")
IFAPOP(" -T tag - enable APOP with this host tag\n")
);
      return 0;
    default:
      return 1;
    }

  if (optind != argc) {
    fprintf(stderr, "%s: extra argument(s) in command line\n", argv[0]);
    return 1;
  }

  if (!authenticated || user)
    ;
  else if ((a = getenv("LOGNAME")) != NULL && *a)
    user = a;
  else {
    struct passwd *pw = getpwuid(getuid());
    if (pw)
      user = strdup(pw->pw_name);
    else {
      snprintf(line, sizeof(line), "#%d", (int)getuid());
      user = strdup(line);
    }
  }

  if (rhost)
    ;
  else if (rhostvar) {
    if ((rhost = getenv(rhostvar)) == NULL || !*rhost) {
      fprintf(stderr, "%s: %s variable not set\n", argv[0], rhostvar);
      return 1;
    }
  }
  else if (((a = getenv("TCPREMOTEIP")) != NULL && *a) ||
	   ((a = getenv("REMOTE_HOST")) != NULL && *a))
    rhost = a;
  else {
    struct sockaddr_in sin;
    i = sizeof(sin);
    if (getpeername(0, (struct sockaddr*)&sin, &i) == 0)
      rhost = strdup(inet_ntoa(sin.sin_addr));
    else if (errno == ENOTSOCK)
      rhost = "local";
    else {
      fprintf(stderr, "%s: unable to get peer name: %m\n", argv[0]);
      return 1;
    }
  }
  doopenlog();
  if (!quiet) syslog(LOG_INFO, "connection from %s", rhost);
  signal(SIGALRM, sig);
  signal(SIGPIPE, sig);

  if (!authenticated) {
#ifdef USE_APOP
    if (apoptag) {
      i = sprintf(line, "<%d.%ld@", (int)getpid(), (long)time(NULL));
      if (apoptag == (char*)-1)
	gethostname(line + i, sizeof(line) - i - 1);
      else {
	if (strlen(apoptag) > 200)
	  apoptag[200] = '\0';
	strcpy(line + i, apoptag);
      }
      i += strlen(line + i);
      if (i > 200) i = 200;
      line[i++] = '>'; line[i] = '\0';
      apoptag = strdup(line);
      putline(OK "%s ready %s", version, apoptag);
    }
    else
#endif
      putline(OK "%s ready", version);
  }
  else
    initmaildir();

  for (;;) {

    alarm(timeout);
    if (!fgets(line,sizeof(line),stdin)) {
      alarm(0);
      if (ferror(stdin))
	syslog(LOG_ERR, "[%s] unable to read from client: %m", rhost);
      else
	syslog(LOG_ERR, "[%s] end of line from client", rhost);
      die("client read error");
    }
    alarm(0);
    i = strlen(line);
    if (i && line[i-1] == '\n')
      line[--i] = '\0';
    if (i && line[i-1] == '\r')
      line[--i] = '\0';
    if (debug) {
      /* do not log user's password */
      if ((line[0] == 'p' || line[0] == 'P') &&
	  (line[1] == 'a' || line[1] == 'A') &&
	  (line[2] == 's' || line[2] == 'S') &&
	  (line[3] == 's' || line[3] == 'S') &&
	  (line[4] == ' '))
	syslog(LOG_DEBUG, "in: %.5s<hidden>", line);
      else
	syslog(LOG_DEBUG, "in: %s", line);
    }
    a = line;
    while(*a && *a != ' ') {
      if (*a >= 'A' && *a <= 'Z')
	*a = *a - 'A' + 'a';
      ++a;
    }
    if (*a == ' ') { 
      *a++ = '\0';
      while(*a == ' ')
	++a;
    }
    if (!*a)
      a = NULL;


    if (!strcmp(line, "quit")) {
      if (authenticated) {
	if (!quiet)
          finallog("client quit");
	finalupdate();
	info();
      }
      else {
	if (!quiet) syslog(LOG_INFO, "[%s] client quit", rhost);
	ok();
      }
      break;
    }

    else if (!strcmp(line, "noop")) {
      if (authenticated) info();
      else ok();
      --junk;
    }

    else if (!strcmp(line, "user")) {
      if (user)
	putline(ERR "user already specified"), --junk;
      else if (!a)
	needarg();
      else if ((user = strdup(a)) == NULL)
	putline(ERR "unable to allocate memory");
      else
	ok();
    }

    else if (!strcmp(line, "pass")) {
      if (!user)
	putline(ERR "user first"), --junk;
      else if (authenticated)
	putline(ERR "already authenticated"), --junk;
      else if (!a)
	needarg(), --junk;
#ifdef USE_APOP
      else if (strlen(a) > 4 && memcmp(a, "APOP ", 5) == 0) {
        syslog(LOG_ERR, "[%s][%s] attempt to use invalid passwd beginning with \"APOP \"",
               rhost, user);
        putline(ERR "invalid passwd");
        break;
      }
#endif
      else if (auth(a) != 0)
	break;
      else
	authenticated = 1;
    }

#ifdef USE_APOP
    else if (apoptag && !strcmp(line, "apop")) {
      char *t;
      if (!a || (t = strchr(a, ' ')) == NULL)
	needarg(), --junk;
      else if (strchr(t + 1, ' '))
	extraarg(), --junk;
      else if (user)
	putline(ERR "user already specified"), --junk;
      else {
	*t++ = '\0';
	user = strdup(a);
	for (a = t; *t; ++t)
	  if ((*t < '0' || *t > '9') && (*t < 'a' || *t > 'f'))
	    break;
	if (*t || t - a != 32) {
	  syslog(LOG_ERR, "[%s][%s] invalid digest for APOP auth command",
		 rhost, user);
	  putline(ERR "invalid passwd md5 digest");
	  break;
	}
	else {
	  /* we use special password here: APOP mdhash apoptag */
	  /* it is safe to use line here (`a' points inside of it) */
	  sprintf(line, "APOP %s %s", a, apoptag);
	  if (auth(line) != 0)
	    break;
	  authenticated = 1;
	}
      }
    }
#endif

    else if (!strcmp(line, "list") || !strcmp(line, "uidl")) {
      int uidl = strcmp(line, "uidl") == 0;
      if (!authenticated)
	needauth(), --junk;
      else if (!a) {
	putmline("+OK");
	for(i = 0; i < nmsg; ++i)
	  if (!(msgp[i]->flag & F_DELETED))
	    dolist(i, uidl, "");
	putmline(".");
	flushmline();
      }
      else if ((i = getmsgno(&a, 0)) < 0)
	--junk;
      else {
	dolist(i, uidl, OK);
	flushmline();
      }
    }

    else if (!strcmp(line, "stat")) {
      if (!authenticated)
	needauth(), --junk;
      else if (a)
	extraarg(), --junk;
      else
	putline(OK "%d %ld", cmsg, (long int)msgsz);
    }

    else if (!strcmp(line, "top") || !strcmp(line, "retr")) {
      int lim = (line[0] == 't') ? 0 : -1; /* -1 - unlimited, 0 - to be */
      FILE *f;
      if (!authenticated)
	needauth(), --junk;
      else if ((i = getmsgno(&a, !lim)) < 0)
	--junk;
      else if (lim && a)
	extraarg(), --junk;
      else if (a && (lim = getnum(&a)) < 0)
	--junk;
      else if (a)
	extraarg(), --junk;
      else if ((f = fopen(msgfn(i), "r")) == NULL)
	putline(ERR "no such message");
      else
	putmsg(f, lim);
    }

    else if (!strcmp(line, "dele")) {
      if (!authenticated)
	needauth(), --junk;
      else if ((i = getmsgno(&a, 0)) >= 0) {
	msgp[i]->flag |= F_DELETED;
	msgsz -= msgp[i]->size;
	--cmsg;
	ok();
      }
    }

    else if (!strcmp(line, "rset")) {
      if (!authenticated)
	needauth(), --junk;
      else if (a)
	extraarg(), --junk;
      else {
	cmsg = nmsg; msgsz = 0;
	for (i = nmsg - 1; i >= 0; --i) {
	  msgp[i]->flag &= ~F_DELETED;
	  msgsz += msgp[i]->size;
	}
	info();
      }
    }

    else if (!strcmp(line, "capa")) {
      if (a)
	extraarg();
      else
	putline(OK "\r\nTOP\r\nUIDL\r\nUSER\r\n\r\n.");
      --junk;
    }

    else
      putline(ERR "command not recognized"), --junk;

    if (!junk) {
      syslog(LOG_NOTICE, "[%s][%s] too many junk commands, giving up",
	     rhost, authenticated ? user : "(none)");
      putline(ERR "too many junk received");
      break;
    }

  }

#ifdef USE_PAM
  if (pamh) {
    pam_close_session(pamh, PAM_SILENT);
    pam_end(pamh, PAM_SUCCESS);
  }
#endif

  return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1