/* $Id: avcheck.c,v 1.21 2002/10/09 22:07:59 mjt Exp $
 * avcheck mail virus scanner client.
 * Michael Tokarev <mjt@corpit.ru>
 * Public domain.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <stdarg.h>
#include <errno.h>
#include <sysexits.h>
#include <sys/wait.h>
#include <signal.h>
#include <syslog.h>

#ifndef INADDR_NONE /* some systems have no INADDR_NONE macro */
# define INADDR_NONE ((unsigned long) -1)
#endif

#ifndef PARANOID
# define PARANOID 1
#endif
#ifndef AVP
# define AVP 1
#endif
#ifndef DRWEB
# define DRWEB 1
#endif
#ifndef SOPHIE
# define SOPHIE 0 /* sophie can't handle MIME properly */
#endif
#ifndef TROPHIE
# define TROPHIE 0  /* trophie can't handle MIME */
#endif

static char *progname;
static char *path;		/* temporary file name */
#define MAXARGS 16
static char *sendmail_argv[MAXARGS];
static int sendmail_argc;

static char buf[8192];	/* i/o buffer */

static void cleanup() {
  if (path)
    unlink(path);
}

static void cleanup_child() {
  /* we don't want to delete these */
  path = NULL;
}

static void err(int errcode, const char *fmt, ...) {
  va_list ap;
  fprintf(stderr, "%s: ", progname);
  va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap);
  if (errcode)
    fprintf(stderr, ": %s", strerror(errcode));
  putc('\n', stderr);
  fflush(stderr);
  cleanup();
  exit(EX_TEMPFAIL);
}

static void warn(const char *fmt, ...) {
  va_list ap;
#ifndef LOG_PERROR
# define LOG_PERROR 0
#endif
  openlog(progname, LOG_PID|LOG_PERROR, LOG_MAIL);
  va_start(ap, fmt);
  vsyslog(LOG_WARNING, fmt, ap);
  va_end(ap);
}

static void
timeout(int unused_sig)
{
  err(0, "timeout waiting for antivirus daemon");
}

static void
sighandler(int sig)
{
  err(0, "got fatal signal %d", sig);
}

static int iwrite(int fd, const char *buf, int len) {
  do {
    int r = write(fd, buf, len);
    if (r < 0)
       return r;
    len -= r;
    buf += r;
  } while(len);
  return 0;
}

#ifdef QUOTEADDR

/* determine if a given localpart (between s and e) needs to be quoted */
static int
is_rfc821_ok(const char *s, const char *e)
{
  if (s == e)
    return 1;
  if (!*s || *s == '.')
    return 0;
  while(s < e) {
    if ((*s & 128) || *s <= ' ' ||
        strchr("<>()[]\\,;:\"", *s) ||
        (s[0] == '.' && s[1] == '.'))
      return 0;
    ++s;
  }
  return 1;
}

/* quote address if needed, allocating memory if alloc is true */

#define quoteaddr(addr) ((const char*)_quoteaddr((addr),0))
#define quoteaddr_alloc(addr) (_quoteaddr((addr),1))

static char *
_quoteaddr(const char *addr, int alloc)
{
  int l, r;
  const char *e = strrchr(addr, '@');
  if (!e)
    e = addr + strlen(addr);
  if (is_rfc821_ok(addr, e))
    return (char*)addr;
  l = 0;
  buf[l++] = '"';
  r = sizeof(buf) - strlen(e) - 2;
  while(addr < e) {
    if (l > r) /* nothing we can do here */
      /*XXX err(EX_UNAVAILABLE, 0, "address is too long");*/
      err(0, "address is too long");
    if ((*addr & 128) || strchr("\r\n\"\\", *addr))
      buf[l++] = '\\';
    buf[l++] = *addr++;
  }
  buf[l++] = '"';
  strcpy(buf + l, e);
  if (alloc) {
    char *b = strdup(buf);
    if (!b)
      err(0, "unable to allocate memory");
    return b;
  }
  else
    return buf;
}

#else /* !QUOTEADDR */

# define quoteaddr(addr) (addr)
# define quoteaddr_alloc(addr) (addr)

#endif


/* read SMTP dialog responce from opened filedescriptor fd,
   expecting exp code */
static void
smtpresp(int fd, int exp)
{
#ifdef DEBUG
  fprintf(stderr, "(expecting SMTP response %d)\n", exp);
#else
  int l;
  char *p;
  l = read(fd, buf, sizeof(buf));
  if (l < 3)
    err(l < 0 ? errno : 0, "unable to read smtp response");
  if (buf[--l] != '\n')
    err(0, "long line in smtp response");
  if (buf[l-1] == '\r') --l;
  buf[l] = '\0';
  l = 0;
  for(p = buf; p < buf+3; ++p)
    if (*p >= '0' && *p <= '9')
      l = l * 10 + (*p - '0');
    else
      break;
  if (*p != ' ' || l != exp)
    err(0, "unexpected smtp response (need %d): %s", exp, buf);
#endif
}

/* issue printf-style SMTP command and wait for response,
   expecting resp */
static void
smtpcmd(FILE *f, int resp, const char *fmt, const char *arg)
{
  if (fprintf(f, fmt, arg) < 0 || fputs("\r\n", f) < 0 || fflush(f) < 0)
    err(errno, "unable to write smtp command");
  smtpresp(fileno(f), resp);
}

/* create and connect TCP socket based on spec.  defport is a
   default port number to use, what used for error messages if any */
static int
tcpsock(const char *spec, int defport, const char *what)
{
  int fd;
  char *h, *p;
  struct sockaddr_in ssin;

  if ((p = strchr(spec, ':')) != NULL) {
    int len = p - spec;
    h = (char*)alloca(len + 1);
    memcpy(h, spec, len);
    h[len] = '\0';
    ++p;
  }
  else
    h = (char*)spec;
  memset(&ssin, 0, sizeof(ssin));
  ssin.sin_family = AF_INET;
  if (!*h)
    h = "127.0.0.1";
  if ((ssin.sin_addr.s_addr = inet_addr(h)) == INADDR_NONE ||
      ssin.sin_addr.s_addr == 0) {
    struct hostent *he = gethostbyname(h);
    if (!he)
      err(0, "host not found: %s", h);
    if (he->h_addrtype != AF_INET)
      err(0, "unexpected address family for %s", h);
    if (he->h_length != sizeof(ssin.sin_addr))
      err(0, "unexpected address length for %s", h);
    memcpy(&ssin.sin_addr, he->h_addr, sizeof(ssin.sin_addr));
  }
  if (!p || !*p)
    ssin.sin_port = htons(defport);
  else if ((defport = atoi(p)) > 0)
    ssin.sin_port = htons(defport);
  else {
    struct servent *se = getservbyname(p, "tcp");
    if (!se)
      err(0, "unknown service %s", p);
    ssin.sin_port = se->s_port;
  }

  fd = socket(AF_INET, SOCK_STREAM, 0);
  if (fd < 0)
    err(errno, "unable to create socket");
  if (connect(fd, (struct sockaddr *)&ssin, sizeof(ssin)) < 0)
    err(errno, "unable to connect to %s", what);

  return fd;

}

/* send a file using external command or smtp-"lite" protocol */
/* If translate is non-zero, then we should "translate" any
   external command error code into EX_TEMPFAIL, thus requiring
   to fork/exec and pipe, waiting to completion */

static void
sendit(int msgfd, char *from, char **to, int nto,
       int translate, char *header) {

  static const char *htemplate = "X-AV-Checked: %.24s %s%s";

  /* open mail stream */

  if (sendmail_argv[0][0] != '/') {
    /* do speak smtp */

    int nl;
    char *bp, *be;
#ifdef DEBUG
    int fd = 2;
    FILE *f = stderr;
    fprintf(stderr, "faking TCP connection to %s\n", sendmail);
#else
    int fd = tcpsock(sendmail_argv[0], 25, "smtp server");
    FILE *f = fdopen(fd, "w");
    if (!f)
      err(errno, "unable to create smtp stream");
#endif
    smtpresp(fd, 220);
    smtpcmd(f, 250, "HELO localhost", NULL);
    smtpcmd(f, 250, "MAIL FROM:<%s>", quoteaddr(from));
    for(nl = 0; nl < nto; ++nl)
      smtpcmd(f, 250, "RCPT TO:<%s>", quoteaddr(to[nl]));
    smtpcmd(f, 354, "DATA", NULL);

    /* copy mail file into smtp stream */
    if (header) {
      time_t now = time(NULL);
      fprintf(f, htemplate, ctime(&now), header, "\r\n");
    }
    /* nl = 1 means we're at a start of a line (watch for dots!) */
    for (nl = 1, bp = be = buf;;) {
      if (bp == be) {
	int l = read(msgfd, buf, sizeof(buf));
	if (l < 0)
	  err(errno, "unable to read");
	if (!l)
	  break;
	bp = buf; be = buf + l;
      }
      if (*bp == '\n') {
	putc('\r', f);
	nl = 1;
      }
      else if (nl) {
	if (*bp == '.')
	  putc('.', f);
	nl = 0;
      }
      if (putc(*bp, f) < 0)
	break;
      ++bp;
    }
    if (!nl)
      fputs("\r\n", f);

    /* finish smtp stream */
    if (fputs(".\r\n", f) < 0 || ferror(f) || fflush(f) < 0)
      err(errno, "unable to write smtp stream");
    smtpresp(fileno(f), 250);
    write(fileno(f), "QUIT\r\n", 6);
#ifndef DEBUG
    read(fileno(f), buf, sizeof(buf)); /* ignore any response */
    fclose(f);
#endif

  }
  else {
    /* execute sendmail program, talk via pipe if translate is non-zero,
       or execute it directly replacing our process */

    int fd, pfd[2];
    int l;
    pid_t cpid = 0;

    /* XXX note: header!=NULL can only happen with translate==true */
    if (translate && (pipe(pfd) != 0 || (cpid = fork()) < 0))
      err(errno, "unable to pipe() or fork()");
    if (!translate || cpid == 0) { /* child */
      char **av = (char**)alloca(sizeof(char*) * (sendmail_argc + nto + 4));
      int ac;
      cleanup_child();
      close(pfd[1]);
      if (pfd[0] != 0) {
	if (dup2(pfd[0], 0) < 0)
	  err(errno, "unable to redirect input");
	close(pfd[0]);
      }
      for(ac = 0; ac < sendmail_argc; ++ac)
	av[ac] = sendmail_argv[ac];
      av[ac++] = "-f";
      av[ac++] = quoteaddr_alloc(from);
      av[ac++] = "--";
      for(l = 0; l < nto; ++l)
	av[ac++] = quoteaddr_alloc(to[l]);
      av[ac] = NULL;
#ifdef DEBUG
      fprintf(stderr, "faking execution of:\n");
      for(l = 0; av[l]; ++l)
	fprintf(stderr, " %s", av[l]);
      putc('\n', stderr);
      while((l = read(0, buf, sizeof(buf))) > 0)
	write(2, buf, l);
      exit(0);
#else
      execvp(sendmail_argv[0], av);
      err(errno, "unable to execute %s", sendmail_argv[0]);
#endif
    }
    close(pfd[0]);
    fd = pfd[1];

    if (header) {
      time_t now = time(NULL);
      snprintf(buf, sizeof(buf) - 1, htemplate, ctime(&now), "");
      l = strlen(buf);
      buf[l++] = '\n'; /* no overflow: \0 at the end */
      if (write(msgfd, header, l) != l)
        err(errno, "unable to write header to mail pipe");
    }
    while((l = read(msgfd, buf, sizeof(buf))) > 0)
      if (write(fd, buf, l) != l)
	err(errno, "unable to write to mail pipe");
    if (l < 0)
      err(errno, "unable to read input");
    if (close(fd) < 0 || wait(&l) < 0)
      err(errno, "unable to finish mail pipe");
    if (l != 0)
      err(0, "unable to send mail: %s returned %d", sendmail_argv[0], l);
  }

}

#if AVP
static int
scan_avp(int fd, const char *path, const char *avname) {

  unsigned short r; /*XXX size/endiannes problem */
  char *p, *t, *n;
  int l, len;
  time_t now = time(NULL);

  l = 3 + 15 + 1 + strlen(path);
  p = (char*)alloca(l + 1);
  sprintf(p, "<0>%.15s:%s", ctime(&now) + 4, path);

  errno = 0;
  if (iwrite(fd, p, l) < 0)
    err(errno, "unable to send command to %s", avname);
  if (read(fd, &r, 2) != 2) /*XXX endiannes problem */
    err(errno, "unable to read %s responce", avname);
  /*XXX avp uses very bogus protocol.
    Here we're ignoring it's text answer(s) unless code says that
    it found some virus(es). */
  switch(r & 0xcf) {
  case 0: /* the only one OK result. */
    /*XXX unfortunately avp is so buggy that it will report success
     if it encountered some error(s), so that this is not really
     "OK result." */
    return 0;
  case 1: /* virus scan was incomplete */
    /* this returned when AVP is unable to parse file structure for
       various reasons. */
  case 8: /* file is corrupted (e.g. archive) - do not treat it as bad */
    warn("unexpected %s return code %d (0x%04x) (%s)", avname, r & 0xcf, r,
         ((r & 0xcf) == 1) ? "virus scan was incomplete" : "file is corrupted");
    return 0;
  case 4: /* known virus(es) detected */
  case 3: /* suspicious object(s) found */
  case 2: /* Found corrupted or changed virus (usually false positive,
             controlled by Warnings=Yes|No in defUnix.prf) */
    break;
  default:
  /* some more additional codes to help diagnose problem(s) */
  if ((r & 0xcf) == 7) p = "kavdaemon is corrupted";
  else if ((r & 0xcf) == 0x0f)
    p = "kavdaemon wants confirmation -- set InfectedAction to 0";
  else if (r & 0x80) p = "kavdaemon integrity failed";
  else if (r & 0x40) p = "kavdaemon av bases not found";
  else if (r & 0x10) p = "kavdaemon key file not found or expired";
  else
    err(0, "unexpected %s return code %d (0x%04x)", avname, r & 0xcf, r);
  err(0, "unexpected %s return code %d (0x%04x) (%s)", avname, r & 0xcf, r, p);
  }
  /* here we're if infected or suspicious object(s) was found by avp */

  /* since we will work here with only one file (message),
     we will not deal with memory deallocation for msg and *msgp. */
  if (!(r & 0x0100)) { /* no message(s) */
    buf[0] = '\0';
    return 1;
  }

  if (r & 0x0200) {
    if (read(fd, &len, 4) != 4) /* read 4-byte junk */
      err(errno, "unable to read %s junk response", avname);
  }

  l = 0;
  if (read(fd, &len, 4) != 4) /*XXX endiannes problem */
    err(errno, "unable to read avp answer length");
  if (len >= sizeof(buf)) /* limit response len to avoid DoSes */
    len = sizeof(buf) - 1;
  p = buf;
  while(len && (l = read(fd, p, len)) > 0) {
    p += l;
    len -= l;
  }
  if (len || l < 0)
    err(errno, "unable to read %s answer", avname);
  *p = '\0';
  /* now interpret avp response. */
  /* we will cut junk from each line before tab character (the only
     interesting information, rest of line is a crap) */
  t = NULL; /* last tab seen */
  p = buf; /* current write pointer */
  n = buf; /* current read pointer */
  for(;;) {
    if (*n == '\t')
      t = ++n;
    else if (*n == '\n' || *n == '\0') {
      if (t && t != n) {
        memcpy(p, t, n - t);
        p += n - t;
        *p++ = '\n';
      }
      if (!*n)
        break;
      ++n;
      t = NULL;
    }
    else
      ++n;
  }
  *p = '\0';
  return 1;
}

#endif /* AVP */

#if DRWEB

static int
scan_drweb(int fd, const char *path, const char *avname) {

/* DrWeb commands */
#define DRWEBD_SCAN_CMD       0x0001

/* DrWeb SCAN_CMD flags */
#define DRWEBD_RETURN_VIRUSES 0x0001
#define DRWEBD_HEURISTIC_ON   0x0008

#define DRWEBD_SCAN_FLAGS (DRWEBD_RETURN_VIRUSES/*|DRWEBD_HEURISTIC_ON*/)

/* DrWeb result codes */
#define DERR_READ_ERR		0x00001
#define DERR_WRITE_ERR		0x00002
#define DERR_NOMEMORY		0x00004
#define DERR_CRC_ERROR		0x00008
#define DERR_READSOCKET		0x00010
#define DERR_KNOWN_VIRUS	0x00020
#define DERR_UNKNOWN_VIRUS	0x00040
#define DERR_VIRUS_MODIFICATION	0x00080
#define DERR_TIMEOUT		0x00200
#define DERR_SYMLINK		0x00400
#define DERR_NO_REGFILE		0x00800
#define DERR_SKIPPED		0x01000
#define DERR_TOO_BIG		0x02000
#define DERR_TOO_COMPRESSED	0x04000
#define DERR_BAD_CALL		0x08000
#define DERR_EVAL_VERSION	0x10000
#define DERR_SPAM_MESSAGE	0x20000

#define DERR_VIRUS \
  (DERR_KNOWN_VIRUS|DERR_UNKNOWN_VIRUS|DERR_VIRUS_MODIFICATION)

  int c, n;
  char *b, *e;

  b = buf;

  c = htonl(DRWEBD_SCAN_CMD); memcpy(b, &c, sizeof(c)); b += sizeof(c);
  c = htonl(DRWEBD_SCAN_FLAGS); memcpy(b, &c, sizeof(c)); b += sizeof(c);
  n = strlen(path);
  c = htonl(n); memcpy(b, &c, sizeof(c)); b += sizeof(c);
  memcpy(b, path, n); b += n;
  c = htonl(0); memcpy(b, &c, sizeof(c)); b += sizeof(c);

  n = b - buf;
  if (iwrite(fd, buf, n) < 0)
    err(errno, "error sending command to %s daemon", avname);

  if (read(fd, &c, sizeof(c)) != sizeof(c) || /* code */
      read(fd, &n, sizeof(n)) != sizeof(n))   /* number of viruses */
    err(errno, "error reading %s daemon response", avname);
  if ((c = ntohl(c)) == 0)
    return 0; /* all ok, no viruses found (n should be 0) */

  if (!(c & DERR_VIRUS)) {
#define DERR_SKIP_CODE (DERR_CRC_ERROR|DERR_EVAL_VERSION|DERR_SKIPPED|DERR_READ_ERR)
    if (!(c & ~DERR_SKIP_CODE)) {
      if (c & DERR_CRC_ERROR)
        e = "crc error (e.g. incomplete attachment)";
      else if (c & DERR_SKIPPED)
        e = "some objects was not scanned (e.g. passwd-protected)";
      else if (c & DERR_READ_ERR)
        e = "read error (e.g. invalid or unknown structure)";
      else
        e = NULL;
      if (e)
        warn("unexpected %s return code %d (0x%x): %s", avname, c, c, e);
      return 0;
    }
    if (c & (DERR_TOO_BIG|DERR_TOO_COMPRESSED|DERR_TIMEOUT)) {
      strcpy(buf, "Message is too complex, possible mailbomb");
      return 1;
    }

    err(0, "unexpected %s return code %d (0x%x)", avname, c, c);
  }

  n = ntohl(n);
  if (!n) {
    buf[0] = '\0';
    return 1;
  }

  b = buf; e = buf + sizeof(buf) - 2;
  while(n) {
    --n;
    if (read(fd, &c, sizeof(c)) != sizeof(c))
      break;
    c = ntohl(c); /* virus name length */
    if (c > e - b) {
      c = e - b;
      n = 0;
    }
    if ((c = read(fd, b, c)) <= 0) {
      if (c < 0)
	break;
      continue;
    }
    b += c - 1;
    *b++ = '\n';
  }
  *b = '\0';

  return 1;

}

#endif /* DRWEB */

#if SOPHIE || TROPHIE

static int
scan_sophie_trophie(int fd, const char *path, const char *avname) {
  int l;
  char *p, *r;

  l = strlen(path);
  memcpy(buf, path, l);
  buf[l++] = '\n';
  if (iwrite(fd, buf, l) < 0)
    err(errno, "unable to send a command to %s", avname);

#define sp_msg "Infected by "
#define sp_extra sizeof(sp_msg)

  r = buf + sp_extra;
  if ((l = read(fd, r, sizeof(buf) - 1 - sp_extra)) < 1)
    err(l < 0 ? errno : 0, "unable to read answer from %s", avname);
  if (*r == '0')
    return 0;
  r[l] = '\0';
  if ((p = strchr(r, '\n')) != NULL)
    *p = '\0';
  p = strchr(r, ':');
  if (*r == '-') {
    if (p) {
      /* skip some known error messages */
      ++p;
      if (memcmp(p, "Error: File was encrypted", 26) == 0) return 0;
      if (memcmp(p, "Error: File corrupted", 21) == 0) return 0;
    }
    err(0, "error in %s: return code %s", avname, r);
  }
  if (p) {
    ++p;
    while(*p == ' ' || *p == '\t') ++p;
    if (*p)
      sprintf(buf, "%s%s", sp_msg, p);
    else
      *buf = '\0';
  }
  else
    *buf = '\0';
  return 1;
}

#endif /* SOPHIE_TROPHIE */

static const struct avengine {
  const char *name;
  char *defsock;
  int (*scanfn)(int, const char *, const char *);
} scanner[] = {
#if AVP
  { "AVP", "/var/run/AvpCtl", scan_avp },
#endif
#if DRWEB
  { "DrWeb", "127.0.0.1:3000", scan_drweb },
#endif
#if SOPHIE
  { "Sophie", "/var/run/sophie", scan_sophie_trophie },
#endif
#if TROPHIE
  { "Trophie", "/var/run/trophie", scan_sophie_trophie },
#endif
  { NULL, NULL, NULL }
};

/* like strtok */
static char *nextword(char *str)
{
  static char *p; /* = NULL */
  static const char *delim = " \t\r\n";
  if (str)
    p = str;
  while(*p && strchr(delim, *p))
    ++p;
  if (!*p)
    return NULL;
  str = p++;
  while(*p && !strchr(delim, *p))
    ++p;
  if (*p)
    *p++ = '\0';
  return str;
}

int main(int argc, char **argv) {
  int c;
  char *p;

  int avfd; /* antivirus daemon socket */
  int msgfd; /* message fd */

  /* options */
  const struct avengine *engine = NULL;
  char *avsocket = NULL; /* av control socket */
  char *tmpdir = NULL; /* file-based access directory */
  char *from = NULL;
  int nosend = 0; /* do not send good mails using sendmail */
  int mailclient = 0; /* emulate mail client for injecting mail */
  char *waitfile = NULL;
  unsigned avtimeout = 0; /* timeout for antivirus daemon operation */
  char *okhdr = NULL; /* header to add for good mails */
  int okcode = 0;

  char *ipath = argv[0];
  char *iscript = NULL;

  if ((progname = strrchr(argv[0], '/')) != NULL)
    argv[0] = ++progname;
  else
    progname = argv[0];

  if (argc == 1 ||
      (argc == 2 &&
       (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")))) {
    printf("\
%s: mail virus-checker version " VERSION ".\n\
Usage:  %s options -- recipient...,  where options are:\n\
 -f sender - sender's envelope address (required)\n\
 -s type[:socket] - antivirus daemon type and it's control\n\
   socket (either /path/to/file or host:port).  Supported\n\
   antivirus engines and default control sockets are:\n",
      progname, progname);
    for (c = 0; scanner[c].name; ++c)
      printf("\t%s\t%s\n", scanner[c].name, scanner[c].defsock);
    printf("\
 -d dir - place files to this temporary directory when\n\
   inspecting (use /some/where/./dir if antivirus is chrooted\n\
   to /some/where) (required)\n\
 -t timeout - timeout, in secounds, to wait for antivirus\n\
   answer (default is 0, i.e. no timeout)\n\
 -n - do not send (reinject) good mail back into the mail system\n\
 -g code - return this code for good, uninfected emails\n\
 -S sendmail_path - /path/to/sendmail-compatible executable\n\
   (possible with args -- either using multi-word value or repeating\n\
   this option to specify additional arguments) or host:port to speak\n\
   subset of SMTP (default is port 25 on localhost)\n\
 -h hdr - prepend \"X-AV-Checked: <time> hdr\" to every checked\n\
   mail message\n\
 -i program - execute this program to handle infected mail\n\
   (default is `infected' in the %s's directory)\n\
 -w waitfile - do not attempt to contact with antivirus\n\
   if waitfile exists but exit with EX_TEMPFAIL instead\n\
   (default is to not perform this check)\n\
 -c - read message from stdin and pass it into mail system\n\
  (using sendmail_path).  In this special mode, %s acts like\n\
  simple transparent mail injection tool (all options except\n\
  of -f (required) and -S are ignored in this mode)\n\
", progname, progname);
    return EX_USAGE;
  }

  while((c = getopt(argc, argv, "t:f:d:s:S:ni:cw:h:g:")) != EOF)
    switch(c) {
    case 't': /* timeout */
      if ((c = atoi(optarg)) < 0)
	err(0, "invalid timeout value `%s'", optarg);
      avtimeout = c;
      break;
    case 's': /* avengine type[:socket] */
      if ((p = strchr(optarg, ':')) != NULL)
	*p++ = '\0';
      c = 0;
      while(strcasecmp(optarg, scanner[c].name) != 0)
	if (!scanner[++c].name)
	  err(0, "unknown antivirus engine type `%s' specified", optarg);
      engine = &scanner[c];
      avsocket = p ? p : engine->defsock;
      break;
    case 'f': from = optarg; break;
    case 'S':
      /* split it into words */
      for (p = nextword(optarg); p != NULL; p = nextword(NULL))
	if (sendmail_argc == MAXARGS)
	  err(0, "too many arguments for -S option");
	else
	  sendmail_argv[sendmail_argc++] = p;
      break;
    case 'd': tmpdir = optarg; break;
    case 'n': nosend = 1; break;
    case 'g':
      if ((c = atoi(optarg)) < 0 || c > 255)
	err(0, "invalid code for -g option `%s'", optarg);
      okcode = c;
      break;
    case 'i': iscript = optarg; break;
    case 'c': mailclient = 1; break;
    case 'w': waitfile = optarg; break;
    case 'h': okhdr = optarg; break;
    default:
      err(0, "execute `%s' without any parameters for help", progname);
    }
  argv += optind;
  argc -= optind;

  if (!argc || !from)
    err(0, "no recipient(s) or sender specified");

  if (!sendmail_argc) {
    sendmail_argc = 1;
    sendmail_argv[0] = ":25";
  }

  if (mailclient) {
    /* only pass message from stdin into mail system */
    sendit(0, from, argv, argc, 0, NULL);
    return 0; /* no cleanup required */
  }

  if (!engine || !tmpdir)
    err(0, "no avtype/socket (-s) or tmpdir (-d) specified");

  if (waitfile && access(waitfile, F_OK) == 0)
    err(0, "antivirus operations temporary disabled (waitfile exists)");

  if (avsocket[0] == '/')
  { /* connect to socket and verify */
    struct sockaddr_un s;
#if PARANOID
    struct stat st;
#endif
    avfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (avfd < 0)
      err(errno, "unable to create socket");
    memset(&s, 0, sizeof(s));
    s.sun_family = AF_UNIX;
    strncpy(s.sun_path, avsocket, sizeof(s.sun_path) - 1);
    if (connect(avfd, (struct sockaddr*)&s,
                sizeof(s.sun_family) + strlen(s.sun_path) + 1) != 0)
      err(errno, "unable to connect to antivirus daemon at %s", avsocket);
#if PARANOID
    if (stat(s.sun_path, &st) != 0)
      err(errno, "unable to fstat() socket");
    if (st.st_uid == 0 || st.st_uid == getuid())
      err(0, "do not allow BUGGY antivirus daemon to run as this user");
#endif
  }
  else
    avfd = tcpsock(avsocket, 0, "av daemon");

  fcntl(avfd, F_SETFD, FD_CLOEXEC);

  /* setup signals to clean things up */
  signal(SIGINT, sighandler);
  signal(SIGHUP, sighandler);

  { /* av file-based mode */

    char *fn; /* file name as passed to av daemon */

    path = (char*)alloca(strlen(tmpdir) + 32);
    sprintf(path, "%s/%ld.tmp", tmpdir, (long)getpid());
    unlink(path); /* just in case -- cleanup */
    umask(027);
    msgfd = open(path, O_CREAT|O_RDWR|O_EXCL, 0640);
    if (msgfd < 0)
      err(errno, "unable to create temp file %s", path);
    fcntl(msgfd, F_SETFD, FD_CLOEXEC);
    while((c = read(0, buf, sizeof(buf))) > 0) {
      int l = write(msgfd, buf, c);
      if (l != c)
	err(l < 0 ? errno : 0, "unable to write temp file");
    }
    if (c < 0)
      err(errno, "unable to read input");
    p = strstr(tmpdir, "/./");
    if (p == NULL) {
#if PARANOID > 1
      err(0,
      "BUGGY antivirus daemon must run chrooted to minimize possible system damage");
#endif
      fn = path;
    }
    else
      fn = path + (p - tmpdir + 2);
    if (avtimeout) {
      signal(SIGALRM, timeout);
      alarm(avtimeout);
    }
    c = (*engine->scanfn)(avfd, fn, engine->name);
    if (avtimeout)
      alarm(0);
  }

  if (c == 0) { /* no viruses found */
    if (!nosend) {
      /* send a message to given recipient(s) */
      if (lseek(msgfd, 0, SEEK_SET) != 0)
	err(errno, "unable to rewind message file");
      sendit(msgfd, from, argv, argc, 1, okhdr);
    }
    cleanup();
    return okcode;
  }
  else { /* else call infected mail handler */
    char **av, **ev;
    int ac;
    static const char sendmail_var[] = "SENDMAIL=";

    /* construct argv */
    av = (char**)alloca((argc + 5) * sizeof(char*));
    ac = 0;
    if (iscript)
      p = iscript;
    else {
      p = (char*)alloca((progname - ipath) + sizeof("infected"));
      memcpy(p, ipath, progname - ipath);
      strcpy(p + (progname - ipath), "infected");
    }
    av[ac++] = p;	/* 0 program name */
    av[ac++] = path;	/* 1 temporary mail file */
    av[ac++] = buf;	/* 2 antivirus message */
    av[ac++] = from;	/* 3 from address */
    for (c = 0; c < argc; ++c) /* rest are recipients */
      av[ac++] = argv[c];
    av[ac] = NULL;

    /* construct environment */
    ev = (char**)alloca((2 + 1) * sizeof(char*));
    ac = 0;
    ev[ac++] = "PATH=/bin:/usr/bin";	/* standard path */
    if (sendmail_argv[0][0] == '/') {
      int len = sizeof(sendmail_var);
      /* collect length of all sendmail command and args */
      for(c = 0; c < sendmail_argc; ++c)
        len += strlen(sendmail_argv[c]) + 1;
      p = (char*)alloca(len);
      len = sprintf(p, "%s%s", sendmail_var, sendmail_argv[0]);
      for(c = 1; c < sendmail_argc; ++c)
        len += sprintf(p + len, " %s", sendmail_argv[c]);
    }
    else {
      p = (char*)alloca(sizeof(sendmail_var) + strlen(ipath) + 4
			+ strlen(sendmail_argv[0]));
      sprintf(p, "%s%s -cS%s", sendmail_var, ipath, sendmail_argv[0]);
    }
    ev[ac++] = p;
    ev[ac] = NULL;

    execve(av[0], av, ev);
    err(errno, "unable to execute `%s'", av[0]);
    return 0;
  }
}
