/* $Cambridge: exim/src/src/malware.c,v 1.21 2010/06/07 00:12:42 pdp Exp $ */

/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-???? */
/* License: GPL */

/* Code for calling virus (malware) scanners. Called from acl.c. */

#include "exim.h"
#ifdef WITH_CONTENT_SCAN

/* declaration of private routines */
static int mksd_scan_packed(int sock, uschar *scan_filename);
static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking);

/* SHUT_WR seems to be undefined on Unixware? */
#ifndef SHUT_WR
#define SHUT_WR 1
#endif


#define        MALWARE_TIMEOUT             120


#define DRWEBD_SCAN_CMD             (1)     /* scan file, buffer or diskfile */
#define DRWEBD_RETURN_VIRUSES       (1<<0)   /* ask daemon return to us viruses names from report */
#define DRWEBD_IS_MAIL              (1<<19)  /* say to daemon that format is "archive MAIL" */

#define DERR_READ_ERR               (1<<0)   /* read error */
#define DERR_NOMEMORY               (1<<2)   /* no memory */
#define DERR_TIMEOUT                (1<<9)   /* scan timeout has run out */
#define DERR_BAD_CALL               (1<<15)  /* wrong command */

/* Routine to check whether a system is big- or litte-endian.
   Ripped from http://www.faqs.org/faqs/graphics/fileformats-faq/part4/section-7.html
   Needed for proper kavdaemon implementation. Sigh. */
#define BIG_MY_ENDIAN      0
#define LITTLE_MY_ENDIAN   1
int test_byte_order(void);
int test_byte_order() {
      short int word = 0x0001;
      char *byte = (char *) &word;
      return(byte[0] ? LITTLE_MY_ENDIAN : BIG_MY_ENDIAN);
}

uschar malware_name_buffer[256];
int malware_ok = 0;

/* Gross hacks for the -bmalware option; perhaps we should just create
the scan directory normally for that case, but look into rigging up the
needed header variables if not already set on the command-line? */
extern int spool_mbox_ok;
extern uschar spooled_message_id[17];

/*************************************************
*          Scan an email for malware             *
*************************************************/

/* This is the normal interface for scanning an email, which doesn't need a
filename; it's a wrapper around the malware_file function.

Arguments:
  listptr     the list of options to the "malware = ..." ACL condition

Returns:      Exim message processing code (OK, FAIL, DEFER, ...)
              where true means malware was found (condition applies)
*/
int malware(uschar **listptr) {
  uschar scan_filename[1024];
  BOOL fits;

  fits = string_format(scan_filename, sizeof(scan_filename),
      CS"%s/scan/%s/%s.eml", spool_directory, message_id, message_id);
  if (!fits)
    {
    log_write(0, LOG_MAIN|LOG_PANIC,
        "malware filename does not fit in buffer [malware()]");
    return DEFER;
  }

  return malware_internal(listptr, scan_filename, FALSE);
}


/*************************************************
*          Scan a file for malware               *
*************************************************/

/* This is a test wrapper for scanning an email, which is not used in
normal processing.  Scan any file, using the Exim scanning interface.
This function tampers with various global variables so is unsafe to use
in any other context.

Arguments:
  eml_filename  a file holding the message to be scanned

Returns:        Exim message processing code (OK, FAIL, DEFER, ...)
                where true means malware was found (condition applies)
*/
int
malware_in_file(uschar *eml_filename) {
  uschar *scan_options[2];
  uschar message_id_buf[64];
  int ret;

  scan_options[0] = US"*";
  scan_options[1] = NULL;

  /* spool_mbox() assumes various parameters exist, when creating
  the relevant directory and the email within */
  (void) string_format(message_id_buf, sizeof(message_id_buf),
      "dummy-%d", pseudo_random_number(INT_MAX));
  message_id = message_id_buf;
  sender_address = US"malware-sender@example.net";
  return_path = US"";
  recipients_list = NULL;
  receive_add_recipient(US"malware-victim@example.net", -1);
  enable_dollar_recipients = TRUE;

  ret = malware_internal(scan_options, eml_filename, TRUE);

  Ustrncpy(spooled_message_id, message_id, sizeof(spooled_message_id));
  spool_mbox_ok = 1;
  /* don't set no_mbox_unspool; at present, there's no way for it to become
  set, but if that changes, then it should apply to these tests too */
  unspool_mbox();

  return ret;
}


/*************************************************
*          Scan content for malware              *
*************************************************/

/* This is an internal interface for scanning an email; the normal interface
is via malware(), or there's malware_in_file() used for testing/debugging.

Arguments:
  listptr       the list of options to the "malware = ..." ACL condition
  eml_filename  the file holding the email to be scanned
  faking        whether or not we're faking this up for the -bmalware test

Returns:        Exim message processing code (OK, FAIL, DEFER, ...)
                where true means malware was found (condition applies)
*/
static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking) {
  int sep = 0;
  uschar *list = *listptr;
  uschar *av_scanner_work = av_scanner;
  uschar *scanner_name;
  uschar scanner_name_buffer[16];
  uschar *malware_regex;
  uschar malware_regex_buffer[64];
  uschar malware_regex_default[] = ".+";
  unsigned long mbox_size;
  FILE *mbox_file;
  int roffset;
  const pcre *re;
  const uschar *rerror;

  /* make sure the eml mbox file is spooled up */
  mbox_file = spool_mbox(&mbox_size, faking ? eml_filename : NULL);
  if (mbox_file == NULL) {
    /* error while spooling */
    log_write(0, LOG_MAIN|LOG_PANIC,
           "malware acl condition: error while creating mbox spool file");
    return DEFER;
  };
  /* none of our current scanners need the mbox
     file as a stream, so we can close it right away */
  (void)fclose(mbox_file);

  /* extract the malware regex to match against from the option list */
  if ((malware_regex = string_nextinlist(&list, &sep,
                                         malware_regex_buffer,
                                         sizeof(malware_regex_buffer))) != NULL) {

    /* parse 1st option */
    if ( (strcmpic(malware_regex,US"false") == 0) ||
         (Ustrcmp(malware_regex,"0") == 0) ) {
      /* explicitly no matching */
      return FAIL;
    };

    /* special cases (match anything except empty) */
    if ( (strcmpic(malware_regex,US"true") == 0) ||
         (Ustrcmp(malware_regex,"*") == 0) ||
         (Ustrcmp(malware_regex,"1") == 0) ) {
      malware_regex = malware_regex_default;
    };
  }
  else {
    /* empty means "don't match anything" */
    return FAIL;
  };

  /* Reset sep that is set by previous string_nextinlist() call */
  sep = 0;

  /* compile the regex, see if it works */
  re = pcre_compile(CS malware_regex, PCRE_COPT, (const char **)&rerror, &roffset, NULL);
  if (re == NULL) {
    log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: regular expression error in '%s': %s at offset %d", malware_regex, rerror, roffset);
    return DEFER;
  };

  /* if av_scanner starts with a dollar, expand it first */
  if (*av_scanner == '$') {
    av_scanner_work = expand_string(av_scanner);
    if (av_scanner_work == NULL) {
      log_write(0, LOG_MAIN|LOG_PANIC,
           "malware acl condition: av_scanner starts with $, but expansion failed: %s", expand_string_message);
      return DEFER;
    }
    else {
      debug_printf("Expanded av_scanner global: %s\n", av_scanner_work);
      /* disable result caching in this case */
      malware_name = NULL;
      malware_ok = 0;
    };
  }

  /* Do not scan twice. */
  if (malware_ok == 0) {

    /* find the scanner type from the av_scanner option */
    if ((scanner_name = string_nextinlist(&av_scanner_work, &sep,
                                          scanner_name_buffer,
                                          sizeof(scanner_name_buffer))) == NULL) {
      /* no scanner given */
      log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: av_scanner configuration variable is empty");
      return DEFER;
    };

  /* "f-protd" scanner type ----------------------------------------------- */
  if (strcmpic(scanner_name, US"f-protd") == 0) {
    uschar *fp_options, *fp_scan_option;
    uschar fp_scan_option_buffer[1024];
    uschar fp_options_buffer[1024];
    uschar fp_options_default[] = "localhost 10200-10204";
    uschar hostname[256];
    unsigned int port, portlow, porthigh, connect_ok=0, detected=0, par_count = 0;
    struct hostent *he;
    struct in_addr in;
    int sock;
    uschar scanrequest[2048], buf[32768], *strhelper, *strhelper2;

    if ((fp_options = string_nextinlist(&av_scanner_work, &sep,
      fp_options_buffer, sizeof(fp_options_buffer))) == NULL) {
      /* no options supplied, use default options */
      fp_options = fp_options_default;
    };

    /* extract host and port part */
    if ( sscanf(CS fp_options, "%s %u-%u", hostname, &portlow, &porthigh) != 3 ) {
      if ( sscanf(CS fp_options, "%s %u", hostname, &portlow) != 2 ) {
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: f-protd: invalid socket '%s'", fp_options);
        return DEFER;
      }
      porthigh = portlow;
    }

    /* Lookup the host */
    if((he = gethostbyname(CS hostname)) == 0) {
      log_write(0, LOG_MAIN|LOG_PANIC,
        "malware acl condition: f-protd: failed to lookup host '%s'", hostname);
      return DEFER;
    }

    in = *(struct in_addr *) he->h_addr_list[0];
    port = portlow;


    /* Open the f-protd TCP socket */
    if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
      log_write(0, LOG_MAIN|LOG_PANIC,
        "malware acl condition: f-protd: unable to acquire socket (%s)",
        strerror(errno));
      return DEFER;
    }

    /* Try to connect to all portslow-high until connection is established */
    for (port = portlow; !connect_ok && port < porthigh; port++) {
      if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) >= 0) {
        connect_ok = 1;
      }
    }

    if ( !connect_ok ) {
      log_write(0, LOG_MAIN|LOG_PANIC,
        "malware acl condition: f-protd: connection to %s, port %u-%u failed (%s)",
        inet_ntoa(in), portlow, porthigh, strerror(errno));
      (void)close(sock);
      return DEFER;
    }

    DEBUG(D_acl) debug_printf("Malware scan: issuing %s GET\n", scanner_name);
    (void)string_format(scanrequest, 1024, CS"GET %s", eml_filename);

    while ((fp_scan_option = string_nextinlist(&av_scanner_work, &sep,
      fp_scan_option_buffer, sizeof(fp_scan_option_buffer))) != NULL) {
      if ( par_count ) {
        Ustrcat(scanrequest, "%20");
      } else {
        Ustrcat(scanrequest, "?");
      }
      Ustrcat(scanrequest, fp_scan_option);
      par_count++;
    }
    Ustrcat(scanrequest, " HTTP/1.0\r\n\r\n");

    /* send scan request */
    if (send(sock, &scanrequest, Ustrlen(scanrequest)+1, 0) < 0) {
      (void)close(sock);
      log_write(0, LOG_MAIN|LOG_PANIC,
        "malware acl condition: f-protd: unable to send command to socket (%s)", scanrequest);
      return DEFER;
    }

    /* We get a lot of empty lines, so we need this hack to check for any data at all */
    while( recv(sock, buf, 1, MSG_PEEK) > 0 ) {
      if ( recv_line(sock, buf, 32768) > 0) {
        if ( Ustrstr(buf, US"<detected type=\"") != NULL ) {
          detected = 1;
        } else if ( detected && (strhelper = Ustrstr(buf, US"<name>")) ) {
          if ((strhelper2 = Ustrstr(buf, US"</name>")) != NULL) {
            *strhelper2 = '\0';
            Ustrcpy(malware_name_buffer, strhelper + 6);
          }
        } else if ( Ustrstr(buf, US"<summary code=\"") ) {
          if ( Ustrstr(buf, US"<summary code=\"11\">") ) {
            malware_name = malware_name_buffer;
          } else {
            malware_name = NULL;
          }
        }
      }
    }
    (void)close(sock);
  }
  /* "drweb" scanner type ----------------------------------------------- */
  /* v0.1 - added support for tcp sockets          */
  /* v0.0 - initial release -- support for unix sockets      */
  else if (strcmpic(scanner_name,US"drweb") == 0) {
    uschar *drweb_options;
    uschar drweb_options_buffer[1024];
    uschar drweb_options_default[] = "/usr/local/drweb/run/drwebd.sock";
    struct sockaddr_un server;
    int sock, result, ovector[30];
    unsigned int port, fsize;
    uschar tmpbuf[1024], *drweb_fbuf;
    uschar drweb_match_string[128];
    int drweb_rc, drweb_cmd, drweb_flags = 0x0000, drweb_fd,
        drweb_vnum, drweb_slen, drweb_fin = 0x0000;
    unsigned long bread;
    uschar hostname[256];
    struct hostent *he;
    struct in_addr in;
    pcre *drweb_re;

    if ((drweb_options = string_nextinlist(&av_scanner_work, &sep,
      drweb_options_buffer, sizeof(drweb_options_buffer))) == NULL) {
      /* no options supplied, use default options */
      drweb_options = drweb_options_default;
    };

    if (*drweb_options != '/') {

      /* extract host and port part */
      if( sscanf(CS drweb_options, "%s %u", hostname, &port) != 2 ) {
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: invalid socket '%s'", drweb_options);
        return DEFER;
      }

      /* Lookup the host */
      if((he = gethostbyname(CS hostname)) == 0) {
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: failed to lookup host '%s'", hostname);
        return DEFER;
      }

      in = *(struct in_addr *) he->h_addr_list[0];

      /* Open the drwebd TCP socket */
      if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: unable to acquire socket (%s)",
          strerror(errno));
        return DEFER;
      }

      if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: connection to %s, port %u failed (%s)",
          inet_ntoa(in), port, strerror(errno));
        return DEFER;
      }

      /* prepare variables */
      drweb_cmd = htonl(DRWEBD_SCAN_CMD);
      drweb_flags = htonl(DRWEBD_RETURN_VIRUSES | DRWEBD_IS_MAIL);

      /* calc file size */
      drweb_fd = open(CS eml_filename, O_RDONLY);
      if (drweb_fd == -1) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: can't open spool file %s: %s",
          eml_filename, strerror(errno));
        return DEFER;
      }
      fsize = lseek(drweb_fd, 0, SEEK_END);
      if (fsize == -1) {
        (void)close(sock);
        (void)close(drweb_fd);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: can't seek spool file %s: %s",
          eml_filename, strerror(errno));
        return DEFER;
      }
      drweb_slen = htonl(fsize);
      lseek(drweb_fd, 0, SEEK_SET);

      DEBUG(D_acl) debug_printf("Malware scan: issuing %s remote scan [%s %u]\n",
          scanner_name, hostname, port);

      /* send scan request */
      if ((send(sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) ||
          (send(sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) ||
          (send(sock, &drweb_fin, sizeof(drweb_fin), 0) < 0) ||
          (send(sock, &drweb_slen, sizeof(drweb_slen), 0) < 0)) {
        (void)close(sock);
        (void)close(drweb_fd);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: unable to send commands to socket (%s)", drweb_options);
        return DEFER;
      }

      drweb_fbuf = (uschar *) malloc (fsize);
      if (!drweb_fbuf) {
        (void)close(sock);
        (void)close(drweb_fd);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: unable to allocate memory %u for file (%s)",
          fsize, eml_filename);
        return DEFER;
      }

      result = read (drweb_fd, drweb_fbuf, fsize);
      if (result == -1) {
        (void)close(sock);
        (void)close(drweb_fd);
        free(drweb_fbuf);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: can't read spool file %s: %s",
          eml_filename, strerror(errno));
        return DEFER;
      }
      (void)close(drweb_fd);

      /* send file body to socket */
      if (send(sock, drweb_fbuf, fsize, 0) < 0) {
        (void)close(sock);
        free(drweb_fbuf);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: unable to send file body to socket (%s)", drweb_options);
        return DEFER;
      }
      (void)close(drweb_fd);
    }
    else {
      /* open the drwebd UNIX socket */
      sock = socket(AF_UNIX, SOCK_STREAM, 0);
      if (sock < 0) {
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: can't open UNIX socket");
        return DEFER;
      }
      server.sun_family = AF_UNIX;
      Ustrcpy(server.sun_path, drweb_options);
      if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: unable to connect to socket (%s). errno=%d", drweb_options, errno);
        return DEFER;
      }

      /* prepare variables */
      drweb_cmd = htonl(DRWEBD_SCAN_CMD);
      drweb_flags = htonl(DRWEBD_RETURN_VIRUSES | DRWEBD_IS_MAIL);
      drweb_slen = htonl(Ustrlen(eml_filename));

      DEBUG(D_acl) debug_printf("Malware scan: issuing %s local scan [%s]\n",
          scanner_name, drweb_options);

      /* send scan request */
      if ((send(sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) ||
          (send(sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) ||
          (send(sock, &drweb_slen, sizeof(drweb_slen), 0) < 0) ||
          (send(sock, eml_filename, Ustrlen(eml_filename), 0) < 0) ||
          (send(sock, &drweb_fin, sizeof(drweb_fin), 0) < 0)) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: unable to send commands to socket (%s)", drweb_options);
        return DEFER;
      }
    }

    /* wait for result */
    if ((bread = recv(sock, &drweb_rc, sizeof(drweb_rc), 0) != sizeof(drweb_rc))) {
      (void)close(sock);
      log_write(0, LOG_MAIN|LOG_PANIC,
        "malware acl condition: drweb: unable to read return code");
      return DEFER;
    }
    drweb_rc = ntohl(drweb_rc);

    if ((bread = recv(sock, &drweb_vnum, sizeof(drweb_vnum), 0) != sizeof(drweb_vnum))) {
      (void)close(sock);
      log_write(0, LOG_MAIN|LOG_PANIC,
        "malware acl condition: drweb: unable to read the number of viruses");
      return DEFER;
    }
    drweb_vnum = ntohl(drweb_vnum);

    /* "virus(es) found" if virus number is > 0 */
    if (drweb_vnum)
    {
      int i;
      uschar pre_malware_nb[256];

      malware_name = malware_name_buffer;

      /* setup default virus name */
      Ustrcpy(malware_name_buffer,"unknown");

      /* read and concatenate virus names into one string */
      for (i=0;i<drweb_vnum;i++)
      {
        /* read the size of report */
        if ((bread = recv(sock, &drweb_slen, sizeof(drweb_slen), 0) != sizeof(drweb_slen))) {
          (void)close(sock);
          log_write(0, LOG_MAIN|LOG_PANIC,
            "malware acl condition: drweb: cannot read report size");
          return DEFER;
        };
        drweb_slen = ntohl(drweb_slen);

        /* read report body */
        if ((bread = recv(sock, tmpbuf, drweb_slen, 0)) != drweb_slen) {
          (void)close(sock);
          log_write(0, LOG_MAIN|LOG_PANIC,
            "malware acl condition: drweb: cannot read report string");
          return DEFER;
        };
        tmpbuf[drweb_slen] = '\0';

        /* set up match regex, depends on retcode */
        Ustrcpy(drweb_match_string, "infected\\swith\\s*(.+?)$");

        drweb_re = pcre_compile( CS drweb_match_string,
          PCRE_COPT,
          (const char **)&rerror,
          &roffset,
          NULL );

        /* try matcher on the line, grab substring */
        result = pcre_exec(drweb_re, NULL, CS tmpbuf, Ustrlen(tmpbuf), 0, 0, ovector, 30);
        if (result >= 2) {
          pcre_copy_substring(CS tmpbuf, ovector, result, 1, CS pre_malware_nb, 255);
        }
        /* the first name we just copy to malware_name */
        if (i==0)
          Ustrcpy(CS malware_name_buffer, CS pre_malware_nb);
        else {
          /* concatenate each new virus name to previous */
          int slen = Ustrlen(malware_name_buffer);
          if (slen < (slen+Ustrlen(pre_malware_nb))) {
            Ustrcat(malware_name_buffer, "/");
            Ustrcat(malware_name_buffer, pre_malware_nb);
          }
        }
      }
    }
    else {
      const char *drweb_s = NULL;

      if (drweb_rc & DERR_READ_ERR) drweb_s = "read error";
      if (drweb_rc & DERR_NOMEMORY) drweb_s = "no memory";
      if (drweb_rc & DERR_TIMEOUT)  drweb_s = "timeout";
      if (drweb_rc & DERR_BAD_CALL) drweb_s = "wrong command";
      /* retcodes DERR_SYMLINK, DERR_NO_REGFILE, DERR_SKIPPED.
       * DERR_TOO_BIG, DERR_TOO_COMPRESSED, DERR_SPAM,
       * DERR_CRC_ERROR, DERR_READSOCKET, DERR_WRITE_ERR
       * and others are ignored */
      if (drweb_s) {
        log_write(0, LOG_MAIN|LOG_PANIC,
          "malware acl condition: drweb: drweb daemon retcode 0x%x (%s)", drweb_rc, drweb_s);
        (void)close(sock);
        return DEFER;
      }
      /* no virus found */
      malware_name = NULL;
    };
    (void)close(sock);
  }
  /* ----------------------------------------------------------------------- */
    else if (strcmpic(scanner_name,US"aveserver") == 0) {
      uschar *kav_options;
      uschar kav_options_buffer[1024];
      uschar kav_options_default[] = "/var/run/aveserver";
      uschar buf[32768];
      struct sockaddr_un server;
      int sock;
      int result;

      if ((kav_options = string_nextinlist(&av_scanner_work, &sep,
                                           kav_options_buffer,
                                           sizeof(kav_options_buffer))) == NULL) {
        /* no options supplied, use default options */
        kav_options = kav_options_default;
      };

      /* open the aveserver socket */
      sock = socket(AF_UNIX, SOCK_STREAM, 0);
      if (sock < 0) {
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: can't open UNIX socket.");
        return DEFER;
      }
      server.sun_family = AF_UNIX;
      Ustrcpy(server.sun_path, kav_options);
      if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to connect to aveserver UNIX socket (%s). errno=%d", kav_options, errno);
        return DEFER;
      }

      /* read aveserver's greeting and see if it is ready (2xx greeting) */
      recv_line(sock, buf, 32768);

      if (buf[0] != '2') {
        /* aveserver is having problems */
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: aveserver is unavailable (Responded: %s).", ((buf[0] != 0) ? buf : (uschar *)"nothing") );
        return DEFER;
      };

      /* prepare our command */
      (void)string_format(buf, 32768, "SCAN bPQRSTUW %s\r\n", eml_filename);

      DEBUG(D_acl) debug_printf("Malware scan: issuing %s SCAN\n", scanner_name);

      /* and send it */
      if (send(sock, buf, Ustrlen(buf), 0) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to write to aveserver UNIX socket (%s)", kav_options);
        return DEFER;
      }

      malware_name = NULL;
      result = 0;
      /* read response lines, find malware name and final response */
      while (recv_line(sock, buf, 32768) > 0) {
        debug_printf("aveserver: %s\n", buf);
        if (buf[0] == '2') {
    break;
  } else if (buf[0] == '5') {
          /* aveserver is having problems */
          log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to scan file %s (Responded: %s).",
       eml_filename, buf);
          result = DEFER;
    break;
  } else if (Ustrncmp(buf,"322",3) == 0) {
          uschar *p = Ustrchr(&buf[4],' ');
          *p = '\0';
          Ustrcpy(malware_name_buffer,&buf[4]);
          malware_name = malware_name_buffer;
  };
      }

      /* prepare our command */
      (void)string_format(buf, 32768, "quit\r\n");

      /* and send it */
      if (send(sock, buf, Ustrlen(buf), 0) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to write to aveserver UNIX socket (%s)", kav_options);
        return DEFER;
      }

      /* read aveserver's greeting and see if it is ready (2xx greeting) */
      recv_line(sock, buf, 32768);

      if (buf[0] != '2') {
        /* aveserver is having problems */
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to quit aveserver dialogue (Responded: %s).", ((buf[0] != 0) ? buf : (uschar *)"nothing") );
        return DEFER;
      };

      (void)close(sock);

      if (result == DEFER) return DEFER;
    }
    /* "fsecure" scanner type ------------------------------------------------- */
    else if (strcmpic(scanner_name,US"fsecure") == 0) {
      uschar *fsecure_options;
      uschar fsecure_options_buffer[1024];
      uschar fsecure_options_default[] = "/var/run/.fsav";
      struct sockaddr_un server;
      int sock, i, j, bread = 0;
      uschar file_name[1024];
      uschar av_buffer[1024];
      pcre *fs_inf;
      static uschar *cmdoptions[] = { US"CONFIGURE\tARCHIVE\t1\n",
                                      US"CONFIGURE\tTIMEOUT\t0\n",
                                      US"CONFIGURE\tMAXARCH\t5\n",
                                      US"CONFIGURE\tMIME\t1\n" };

      malware_name = NULL;
      if ((fsecure_options = string_nextinlist(&av_scanner_work, &sep,
                                               fsecure_options_buffer,
                                               sizeof(fsecure_options_buffer))) == NULL) {
         /* no options supplied, use default options */
         fsecure_options = fsecure_options_default;
      };

      /* open the fsecure socket */
      sock = socket(AF_UNIX, SOCK_STREAM, 0);
      if (sock < 0) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: unable to open fsecure socket %s (%s)",
                  fsecure_options, strerror(errno));
        return DEFER;
      }
      server.sun_family = AF_UNIX;
      Ustrcpy(server.sun_path, fsecure_options);
      if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: unable to connect to fsecure socket %s (%s)",
                  fsecure_options, strerror(errno));
        return DEFER;
      }

      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
          scanner_name, fsecure_options);

      /* pass options */
      memset(av_buffer, 0, sizeof(av_buffer));
      for (i=0; i != 4; i++) {
        /* debug_printf("send option \"%s\"",cmdoptions[i]); */
        if (write(sock, cmdoptions[i], Ustrlen(cmdoptions[i])) < 0) {
          (void)close(sock);
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: unable to write fsecure option %d to %s (%s)",
                    i, fsecure_options, strerror(errno));
          return DEFER;
        };

        bread = ip_recv(sock, av_buffer, sizeof(av_buffer), MALWARE_TIMEOUT);
        if (bread >0) av_buffer[bread]='\0';
        if (bread < 0) {
          (void)close(sock);
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: unable to read fsecure answer %d (%s)", i, strerror(errno));
          return DEFER;
        };
        for (j=0;j<bread;j++) if((av_buffer[j]=='\r')||(av_buffer[j]=='\n')) av_buffer[j] ='@';
        /* debug_printf("read answer %d read=%d \"%s\"\n", i, bread, av_buffer ); */
        /* while (Ustrstr(av_buffer, "OK\tServer configured.@") == NULL); */
      };

      /* pass the mailfile to fsecure */
      (void)string_format(file_name,1024,"SCAN\t%s\n", eml_filename);
      /* debug_printf("send scan %s",file_name); */
      if (write(sock, file_name, Ustrlen(file_name)) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: unable to write fsecure scan to %s (%s)",
                  fsecure_options, strerror(errno));
        return DEFER;
      };

      /* set up match */
      /* todo also SUSPICION\t */
      fs_inf = pcre_compile("\\S{0,5}INFECTED\\t[^\\t]*\\t([^\\t]+)\\t\\S*$", PCRE_COPT, (const char **)&rerror, &roffset, NULL);

      /* read report, linewise */
      do {
        int ovector[30];
        i = 0;
        memset(av_buffer, 0, sizeof(av_buffer));
        do {
          bread=ip_recv(sock, &av_buffer[i], 1, MALWARE_TIMEOUT);
          if (bread < 0) {
            (void)close(sock);
            log_write(0, LOG_MAIN|LOG_PANIC,
                      "malware acl condition: unable to read fsecure result (%s)", strerror(errno));
            return DEFER;
          };
          i++;
        }
        while ((i < sizeof(av_buffer)-1 ) && (av_buffer[i-1] != '\n'));
        av_buffer[i-1] = '\0';
        /* debug_printf("got line \"%s\"\n",av_buffer); */

        /* Really search for virus again? */
        if (malware_name == NULL) {
          /* try matcher on the line, grab substring */
          i = pcre_exec(fs_inf, NULL, CS av_buffer, Ustrlen(av_buffer), 0, 0, ovector, 30);
          if (i >= 2) {
            /* Got it */
            pcre_copy_substring(CS av_buffer, ovector, i, 1, CS malware_name_buffer, 255);
            malware_name = malware_name_buffer;
          };
        };
      }
      while (Ustrstr(av_buffer, "OK\tScan ok.") == NULL);
      (void)close(sock);
    }
    /* ----------------------------------------------------------------------- */

    /* "kavdaemon" scanner type ------------------------------------------------ */
    else if (strcmpic(scanner_name,US"kavdaemon") == 0) {
      uschar *kav_options;
      uschar kav_options_buffer[1024];
      uschar kav_options_default[] = "/var/run/AvpCtl";
      struct sockaddr_un server;
      int sock;
      time_t t;
      uschar tmpbuf[1024];
      uschar scanrequest[1024];
      uschar kav_match_string[128];
      int kav_rc;
      unsigned long kav_reportlen, bread;
      pcre *kav_re;
      uschar *p;
      int fits;

      if ((kav_options = string_nextinlist(&av_scanner_work, &sep,
                                           kav_options_buffer,
                                           sizeof(kav_options_buffer))) == NULL) {
        /* no options supplied, use default options */
        kav_options = kav_options_default;
      };

      /* open the kavdaemon socket */
      sock = socket(AF_UNIX, SOCK_STREAM, 0);
      if (sock < 0) {
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: can't open UNIX socket.");
        return DEFER;
      }
      server.sun_family = AF_UNIX;
      Ustrcpy(server.sun_path, kav_options);
      if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to connect to kavdaemon UNIX socket (%s). errno=%d", kav_options, errno);
        return DEFER;
      }

      /* get current date and time, build scan request */
      time(&t);
      /* pdp note: before the eml_filename parameter, this scanned the
      directory; not finding documentation, so we'll strip off the directory.
      The side-effect is that the test framework scanning may end up in
      scanning more than was requested, but for the normal interface, this is
      fine. */
      strftime(CS tmpbuf, sizeof(tmpbuf), "<0>%d %b %H:%M:%S:%%s", localtime(&t));
      fits = string_format(scanrequest, 1024,CS tmpbuf, eml_filename);
      if (!fits) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
            "malware filename does not fit in buffer [malware_internal() kavdaemon]");
      }
      p = Ustrrchr(scanrequest, '/');
      if (p)
        *p = '\0';

      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
          scanner_name, kav_options);

      /* send scan request */
      if (send(sock, scanrequest, Ustrlen(scanrequest)+1, 0) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to write to kavdaemon UNIX socket (%s)", kav_options);
        return DEFER;
      }

      /* wait for result */
      if ((bread = recv(sock, tmpbuf, 2, 0) != 2)) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to read 2 bytes from kavdaemon socket.");
        return DEFER;
      }

      /* get errorcode from one nibble */
      if (test_byte_order() == LITTLE_MY_ENDIAN) {
        kav_rc = tmpbuf[0] & 0x0F;
      }
      else {
        kav_rc = tmpbuf[1] & 0x0F;
      };

      /* improper kavdaemon configuration */
      if ( (kav_rc == 5) || (kav_rc == 6) ) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: please reconfigure kavdaemon to NOT disinfect or remove infected files.");
        return DEFER;
      };

      if (kav_rc == 1) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: kavdaemon reported 'scanning not completed' (code 1).");
        return DEFER;
      };

      if (kav_rc == 7) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: kavdaemon reported 'kavdaemon damaged' (code 7).");
        return DEFER;
      };

      /* code 8 is not handled, since it is ambigous. It appears mostly on
      bounces where part of a file has been cut off */

      /* "virus found" return codes (2-4) */
      if ((kav_rc > 1) && (kav_rc < 5)) {
        int report_flag = 0;

        /* setup default virus name */
        Ustrcpy(malware_name_buffer,"unknown");
        malware_name = malware_name_buffer;

        if (test_byte_order() == LITTLE_MY_ENDIAN) {
          report_flag = tmpbuf[1];
        }
        else {
          report_flag = tmpbuf[0];
        };

        /* read the report, if available */
        if( report_flag == 1 ) {
          /* read report size */
          if ((bread = recv(sock, &kav_reportlen, 4, 0)) != 4) {
            (void)close(sock);
            log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: cannot read report size from kavdaemon");
            return DEFER;
          };

          /* it's possible that avp returns av_buffer[1] == 1 but the
          reportsize is 0 (!?) */
          if (kav_reportlen > 0) {
            /* set up match regex, depends on retcode */
            if( kav_rc == 3 )
              Ustrcpy(kav_match_string, "suspicion:\\s*(.+?)\\s*$");
            else
              Ustrcpy(kav_match_string, "infected:\\s*(.+?)\\s*$");

            kav_re = pcre_compile( CS kav_match_string,
                                   PCRE_COPT,
                                   (const char **)&rerror,
                                   &roffset,
                                   NULL );

            /* read report, linewise */
            while (kav_reportlen > 0) {
              int result = 0;
              int ovector[30];

              bread = 0;
              while ( recv(sock, &tmpbuf[bread], 1, 0) == 1 ) {
                kav_reportlen--;
                if ( (tmpbuf[bread] == '\n') || (bread > 1021) ) break;
                bread++;
              };
              bread++;
              tmpbuf[bread] = '\0';

              /* try matcher on the line, grab substring */
              result = pcre_exec(kav_re, NULL, CS tmpbuf, Ustrlen(tmpbuf), 0, 0, ovector, 30);
              if (result >= 2) {
                pcre_copy_substring(CS tmpbuf, ovector, result, 1, CS malware_name_buffer, 255);
                break;
              };
            };
          };
        };
      }
      else {
        /* no virus found */
        malware_name = NULL;
      };

      (void)close(sock);
    }
    /* ----------------------------------------------------------------------- */


    /* "cmdline" scanner type ------------------------------------------------ */
    else if (strcmpic(scanner_name,US"cmdline") == 0) {
      uschar *cmdline_scanner;
      uschar cmdline_scanner_buffer[1024];
      uschar *cmdline_trigger;
      uschar cmdline_trigger_buffer[1024];
      const pcre *cmdline_trigger_re;
      uschar *cmdline_regex;
      uschar cmdline_regex_buffer[1024];
      const pcre *cmdline_regex_re;
      uschar file_name[1024];
      uschar commandline[1024];
      void (*eximsigchld)(int);
      void (*eximsigpipe)(int);
      FILE *scanner_out = NULL;
      FILE *scanner_record = NULL;
      uschar linebuffer[32767];
      int trigger = 0;
      int result;
      int ovector[30];
      uschar *p;
      BOOL fits;

      /* find scanner command line */
      if ((cmdline_scanner = string_nextinlist(&av_scanner_work, &sep,
                                          cmdline_scanner_buffer,
                                          sizeof(cmdline_scanner_buffer))) == NULL) {
        /* no command line supplied */
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: missing commandline specification for cmdline scanner type.");
        return DEFER;
      };

      /* find scanner output trigger */
      if ((cmdline_trigger = string_nextinlist(&av_scanner_work, &sep,
                                          cmdline_trigger_buffer,
                                          sizeof(cmdline_trigger_buffer))) == NULL) {
        /* no trigger regex supplied */
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: missing trigger specification for cmdline scanner type.");
        return DEFER;
      };

      /* precompile trigger regex */
      cmdline_trigger_re = pcre_compile(CS cmdline_trigger, PCRE_COPT, (const char **)&rerror, &roffset, NULL);
      if (cmdline_trigger_re == NULL) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                 "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_trigger_re, rerror, roffset);
        return DEFER;
      };

      /* find scanner name regex */
      if ((cmdline_regex = string_nextinlist(&av_scanner_work, &sep,
                                             cmdline_regex_buffer,
                                             sizeof(cmdline_regex_buffer))) == NULL) {
        /* no name regex supplied */
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: missing virus name regex specification for cmdline scanner type.");
        return DEFER;
      };

      /* precompile name regex */
      cmdline_regex_re = pcre_compile(CS cmdline_regex, PCRE_COPT, (const char **)&rerror, &roffset, NULL);
      if (cmdline_regex_re == NULL) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                 "malware acl condition: regular expression error in '%s': %s at offset %d", cmdline_regex_re, rerror, roffset);
        return DEFER;
      };

      /* prepare scanner call; despite the naming, file_name holds a directory
      name which is documented as the value given to %s. */
      if (Ustrlen(eml_filename) > sizeof(file_name) - 1)
        {
        log_write(0, LOG_MAIN|LOG_PANIC,
            "malware filename does not fit in buffer [malware_internal() cmdline]");
        return DEFER;
        }
      Ustrcpy(file_name, eml_filename);
      p = Ustrrchr(file_name, '/');
      if (p)
        *p = '\0';
      fits = string_format(commandline, sizeof(commandline), CS cmdline_scanner, file_name);
      if (!fits)
        {
        log_write(0, LOG_MAIN|LOG_PANIC,
            "cmdline scanner command-line does not fit in buffer");
        return DEFER;
        }

      /* redirect STDERR too */
      if (Ustrlen(commandline) + 5 > sizeof(commandline))
        {
        log_write(0, LOG_MAIN|LOG_PANIC,
            "cmdline scanner command-line does not fit in buffer (STDERR redirect)");
        return DEFER;
        }
      Ustrcat(commandline," 2>&1");

      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n", scanner_name, commandline);

      /* store exims signal handlers */
      eximsigchld = signal(SIGCHLD,SIG_DFL);
      eximsigpipe = signal(SIGPIPE,SIG_DFL);

      scanner_out = popen(CS commandline,"r");
      if (scanner_out == NULL) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                 "malware acl condition: calling cmdline scanner (%s) failed: %s.", commandline, strerror(errno));
        signal(SIGCHLD,eximsigchld);
        signal(SIGPIPE,eximsigpipe);
        return DEFER;
      };

      (void)string_format(file_name,1024,"%s/scan/%s/%s_scanner_output", spool_directory, message_id, message_id);
      scanner_record = modefopen(file_name,"wb",SPOOL_MODE);

      if (scanner_record == NULL) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                 "malware acl condition: opening scanner output file (%s) failed: %s.", file_name, strerror(errno));
        pclose(scanner_out);
        signal(SIGCHLD,eximsigchld);
        signal(SIGPIPE,eximsigpipe);
        return DEFER;
      };

      /* look for trigger while recording output */
      while(fgets(CS linebuffer,32767,scanner_out) != NULL) {
        if ( Ustrlen(linebuffer) > fwrite(linebuffer, 1, Ustrlen(linebuffer), scanner_record) ) {
          /* short write */
          log_write(0, LOG_MAIN|LOG_PANIC,
                 "malware acl condition: short write on scanner output file (%s).", file_name);
          pclose(scanner_out);
          signal(SIGCHLD,eximsigchld);
          signal(SIGPIPE,eximsigpipe);
          return DEFER;
        };
        /* try trigger match */
        if (!trigger && regex_match_and_setup(cmdline_trigger_re, linebuffer, 0, -1))
          trigger = 1;
      };

      (void)fclose(scanner_record);
      pclose(scanner_out);
      signal(SIGCHLD,eximsigchld);
      signal(SIGPIPE,eximsigpipe);

      if (trigger) {
        /* setup default virus name */
        Ustrcpy(malware_name_buffer,"unknown");
        malware_name = malware_name_buffer;

        /* re-open the scanner output file, look for name match */
        scanner_record = fopen(CS file_name,"rb");
        while(fgets(CS linebuffer,32767,scanner_record) != NULL) {
          /* try match */
          result = pcre_exec(cmdline_regex_re, NULL, CS linebuffer, Ustrlen(linebuffer), 0, 0, ovector, 30);
          if (result >= 2) {
            pcre_copy_substring(CS linebuffer, ovector, result, 1, CS malware_name_buffer, 255);
          };
        };
        (void)fclose(scanner_record);
      }
      else {
        /* no virus found */
        malware_name = NULL;
      };
    }
    /* ----------------------------------------------------------------------- */


    /* "sophie" scanner type ------------------------------------------------- */
    else if (strcmpic(scanner_name,US"sophie") == 0) {
      uschar *sophie_options;
      uschar sophie_options_buffer[1024];
      uschar sophie_options_default[] = "/var/run/sophie";
      int bread = 0;
      struct sockaddr_un server;
      int sock, len;
      uschar *p;
      uschar file_name[1024];
      uschar av_buffer[1024];

      if ((sophie_options = string_nextinlist(&av_scanner_work, &sep,
                                          sophie_options_buffer,
                                          sizeof(sophie_options_buffer))) == NULL) {
        /* no options supplied, use default options */
        sophie_options = sophie_options_default;
      };

      /* open the sophie socket */
      sock = socket(AF_UNIX, SOCK_STREAM, 0);
      if (sock < 0) {
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: can't open UNIX socket.");
        return DEFER;
      }
      server.sun_family = AF_UNIX;
      Ustrcpy(server.sun_path, sophie_options);
      if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to connect to sophie UNIX socket (%s). errno=%d", sophie_options, errno);
        return DEFER;
      }

      /* pass the scan directory to sophie */
      len = Ustrlen(eml_filename) + 1;
      if (len > sizeof(file_name))
        {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
            "malware filename does not fit in buffer [malware_internal() sophie]");
        return DEFER;
        }
      memcpy(file_name, eml_filename, len);
      p = Ustrrchr(file_name, '/');
      if (p)
        *p = '\0';

      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
          scanner_name, sophie_options);

      if (write(sock, file_name, Ustrlen(file_name)) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to write to sophie UNIX socket (%s)", sophie_options);
        return DEFER;
      };

      (void)write(sock, "\n", 1);

      /* wait for result */
      memset(av_buffer, 0, sizeof(av_buffer));
      if ((!(bread = ip_recv(sock, av_buffer, sizeof(av_buffer), MALWARE_TIMEOUT)) > 0)) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to read from sophie UNIX socket (%s)", sophie_options);
        return DEFER;
      };

      (void)close(sock);

      /* infected ? */
      if (av_buffer[0] == '1') {
        if (Ustrchr(av_buffer, '\n')) *Ustrchr(av_buffer, '\n') = '\0';
        Ustrcpy(malware_name_buffer,&av_buffer[2]);
        malware_name = malware_name_buffer;
      }
      else if (!strncmp(CS av_buffer, "-1", 2)) {
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: malware acl condition: sophie reported error");
        return DEFER;
      }
      else {
        /* all ok, no virus */
        malware_name = NULL;
      };
    }
    /* ----------------------------------------------------------------------- */


    /* "clamd" scanner type ------------------------------------------------- */
    /* This code was originally contributed by David Saez */
    /* There are three scanning methods available to us:
     *  (1) Use the SCAN command, pointing to a file in the filesystem
     *  (2) Use the STREAM command, send the data on a separate port
     *  (3) Use the zINSTREAM command, send the data inline
     * The zINSTREAM command was introduced with ClamAV 0.95, which marked
     * STREAM deprecated; see: http://wiki.clamav.net/bin/view/Main/UpgradeNotes095
     * In Exim, we use SCAN if using a Unix-domain socket or explicitly told that
     * the TCP-connected daemon is actually local; otherwise we use zINSTREAM unless
     * WITH_OLD_CLAMAV_STREAM is defined.
     * See Exim bug 926 for details.  */
    else if (strcmpic(scanner_name,US"clamd") == 0) {
      uschar *clamd_options;
      uschar clamd_options_buffer[1024];
      uschar clamd_options_default[] = "/tmp/clamd";
      uschar *p, *vname, *result_tag, *response_end;
      struct sockaddr_un server;
      int sock,bread=0;
      unsigned int port;
      uschar file_name[1024];
      uschar av_buffer[1024];
      uschar hostname[256];
      struct hostent *he;
      struct in_addr in;
      uschar *clamd_options2;
      uschar clamd_options2_buffer[1024];
      uschar clamd_options2_default[] = "";
      uschar *clamav_fbuf;
      int clam_fd, result;
      unsigned int fsize;
      BOOL use_scan_command, fits;
#ifdef WITH_OLD_CLAMAV_STREAM
      uschar av_buffer2[1024];
      int sockData;
#else
      uint32_t send_size, send_final_zeroblock;
#endif

      if ((clamd_options = string_nextinlist(&av_scanner_work, &sep,
                                             clamd_options_buffer,
                                             sizeof(clamd_options_buffer))) == NULL) {
        /* no options supplied, use default options */
        clamd_options = clamd_options_default;
      }
      if ((clamd_options2 = string_nextinlist(&av_scanner_work, &sep,
                                             clamd_options2_buffer,
                                             sizeof(clamd_options2_buffer))) == NULL) {
        clamd_options2 = clamd_options2_default;
      }

      if ((*clamd_options == '/') || (strcmpic(clamd_options2,US"local") == 0))
        use_scan_command = TRUE;
      else
        use_scan_command = FALSE;

      /* See the discussion of response formats below to see why we really don't
      like colons in filenames when passing filenames to ClamAV. */
      if (use_scan_command && Ustrchr(eml_filename, ':')) {
	log_write(0, LOG_MAIN|LOG_PANIC,
	    "malware acl condition: clamd: local/SCAN mode incompatible with" \
	    " : in path to email filename [%s]", eml_filename);
	return DEFER;
      }

      /* socket does not start with '/' -> network socket */
      if (*clamd_options != '/') {

        /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd
         * only supports AF_INET, but we should probably be looking to the
         * future and rewriting this to be protocol-independent anyway. */

        /* extract host and port part */
        if( sscanf(CS clamd_options, "%s %u", hostname, &port) != 2 ) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: clamd: invalid socket '%s'", clamd_options);
          return DEFER;
        };

        /* Lookup the host */
        if((he = gethostbyname(CS hostname)) == 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: clamd: failed to lookup host '%s'", hostname);
          return DEFER;
        }

        in = *(struct in_addr *) he->h_addr_list[0];

        /* Open the ClamAV Socket */
        if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: clamd: unable to acquire socket (%s)",
                    strerror(errno));
          return DEFER;
        }

        if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) {
          (void)close(sock);
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: clamd: connection to %s, port %u failed (%s)",
                    inet_ntoa(in), port, strerror(errno));
          return DEFER;
        }

      } else {
        /* open the local socket */
        if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: clamd: unable to acquire socket (%s)",
                    strerror(errno));
          return DEFER;
        }

        server.sun_family = AF_UNIX;
        Ustrcpy(server.sun_path, clamd_options);

        if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
          (void)close(sock);
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: clamd: unable to connect to UNIX socket %s (%s)",
                    clamd_options, strerror(errno) );
          return DEFER;
        }
      }

      /* have socket in variable "sock"; command to use is semi-independent of
       * the socket protocol.  We use SCAN if is local (either Unix/local
       * domain socket, or explicitly told local) else we stream the data.
       * How we stream the data depends upon how we were built.  */

      if (!use_scan_command) {

#ifdef WITH_OLD_CLAMAV_STREAM
        /* "STREAM\n" command, get back a "PORT <N>\n" response, send data to
         * that port on a second connection; then in the scan-method-neutral
         * part, read the response back on the original connection. */

        DEBUG(D_acl) debug_printf("Malware scan: issuing %s old-style remote scan (PORT)\n",
            scanner_name);

        /* Pass the string to ClamAV (7 = "STREAM\n") */
        if (send(sock, "STREAM\n", 7, 0) < 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,"malware acl condition: clamd: unable to write to socket (%s)",
                strerror(errno));
          (void)close(sock);
          return DEFER;
        }
        memset(av_buffer2, 0, sizeof(av_buffer2));
        bread = ip_recv(sock, av_buffer2, sizeof(av_buffer2), MALWARE_TIMEOUT);

        if (bread < 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                "malware acl condition: clamd: unable to read PORT from socket (%s)",
                strerror(errno));
          (void)close(sock);
          return DEFER;
        }

        if (bread == sizeof(av_buffer)) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                "malware acl condition: clamd: buffer too small");
          (void)close(sock);
          return DEFER;
        }

        if (!(*av_buffer2)) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                "malware acl condition: clamd: ClamAV returned null");
          (void)close(sock);
          return DEFER;
        }

        av_buffer2[bread] = '\0';
        if( sscanf(CS av_buffer2, "PORT %u\n", &port) != 1 ) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: clamd: Expected port information from clamd, got '%s'", av_buffer2);
          (void)close(sock);
          return DEFER;
        };

        if ( (sockData = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: clamd: unable to acquire socket (%s)",
                  strerror(errno));
          (void)close(sock);
          return DEFER;
        }

        if (ip_connect(sockData, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: clamd: connection to %s, port %u failed (%s)",
                  inet_ntoa(in), port, strerror(errno));
          (void)close(sockData); (void)close(sock);
          return DEFER;
        }

#define CLOSE_SOCKDATA (void)close(sockData)
#else /* WITH_OLD_CLAMAV_STREAM not defined */
        /* New protocol: "zINSTREAM\n" followed by a sequence of <length><data>
        chunks, <n> a 4-byte number (network order), terminated by a zero-length
        chunk. */

        DEBUG(D_acl) debug_printf("Malware scan: issuing %s new-style remote scan (zINSTREAM)\n",
            scanner_name);

        /* Pass the string to ClamAV (10 = "zINSTREAM\0") */
        if (send(sock, "zINSTREAM", 10, 0) < 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,
              "malware acl condition: clamd: unable to send zINSTREAM to socket (%s)",
              strerror(errno));
          (void)close(sock);
          return DEFER;
        }

#define CLOSE_SOCKDATA /**/
#endif

        /* calc file size */
        clam_fd = open(CS eml_filename, O_RDONLY);
        if (clam_fd == -1) {
          log_write(0, LOG_MAIN|LOG_PANIC,
            "malware acl condition: clamd: can't open spool file %s: %s",
            eml_filename, strerror(errno));
          CLOSE_SOCKDATA; (void)close(sock);
          return DEFER;
        }
        fsize = lseek(clam_fd, 0, SEEK_END);
        if (fsize == -1) {
          log_write(0, LOG_MAIN|LOG_PANIC,
            "malware acl condition: clamd: can't seek spool file %s: %s",
            eml_filename, strerror(errno));
          CLOSE_SOCKDATA; (void)close(sock);
          return DEFER;
        }
        lseek(clam_fd, 0, SEEK_SET);

        clamav_fbuf = (uschar *) malloc (fsize);
        if (!clamav_fbuf) {
          log_write(0, LOG_MAIN|LOG_PANIC,
            "malware acl condition: clamd: unable to allocate memory %u for file (%s)",
            fsize, eml_filename);
          CLOSE_SOCKDATA; (void)close(sock); (void)close(clam_fd);
          return DEFER;
        }

        result = read (clam_fd, clamav_fbuf, fsize);
        if (result == -1) {
          log_write(0, LOG_MAIN|LOG_PANIC,
            "malware acl condition: clamd: can't read spool file %s: %s",
            eml_filename, strerror(errno));
          CLOSE_SOCKDATA; (void)close(sock); (void)close(clam_fd);
          free(clamav_fbuf);
          return DEFER;
        }
        (void)close(clam_fd);

        /* send file body to socket */
#ifdef WITH_OLD_CLAMAV_STREAM
        if (send(sockData, clamav_fbuf, fsize, 0) < 0) {
          log_write(0, LOG_MAIN|LOG_PANIC,
            "malware acl condition: clamd: unable to send file body to socket (%s:%u)", hostname, port);
          CLOSE_SOCKDATA; (void)close(sock);
          free(clamav_fbuf);
          return DEFER;
        }
#else
        send_size = htonl(fsize);
        send_final_zeroblock = 0;
        if ((send(sock, &send_size, sizeof(send_size), 0) < 0) ||
            (send(sock, clamav_fbuf, fsize, 0) < 0) ||
            (send(sock, &send_final_zeroblock, sizeof(send_final_zeroblock), 0) < 0))
          {
          log_write(0, LOG_MAIN|LOG_PANIC,
            "malware acl condition: clamd: unable to send file body to socket (%s:%u)", hostname, port);
          (void)close(sock);
          free(clamav_fbuf);
          return DEFER;
          }
#endif

        free(clamav_fbuf);

        CLOSE_SOCKDATA;
#undef CLOSE_SOCKDATA

      } else { /* use scan command */
        /* Send a SCAN command pointing to a filename; then in the then in the
         * scan-method-neutral part, read the response back */

/* ================================================================= */

        /* Prior to the reworking post-Exim-4.72, this scanned a directory,
        which dates to when ClamAV needed us to break apart the email into the
        MIME parts (eg, with the now deprecated demime condition coming first).
        Some time back, ClamAV gained the ability to deconstruct the emails, so
        doing this would actually have resulted in the mail attachments being
        scanned twice, in the broken out files and from the original .eml.
        Since ClamAV now handles emails (and has for quite some time) we can
        just use the email file itself. */
        /* Pass the string to ClamAV (7 = "SCAN \n" + \0) */
        fits = string_format(file_name, sizeof(file_name), "SCAN %s\n",
            eml_filename);
        if (!fits) {
          (void)close(sock);
          log_write(0, LOG_MAIN|LOG_PANIC,
              "malware filename does not fit in buffer [malware_internal() clamd]");
        }

        DEBUG(D_acl) debug_printf("Malware scan: issuing %s local-path scan [%s]\n",
            scanner_name, clamd_options);

        if (send(sock, file_name, Ustrlen(file_name), 0) < 0) {
          (void)close(sock);
          log_write(0, LOG_MAIN|LOG_PANIC,"malware acl condition: clamd: unable to write to socket (%s)",
                    strerror(errno));
          return DEFER;
        }

        /* Do not shut down the socket for writing; a user report noted that
         * clamd 0.70 does not react well to this. */
      }
      /* Commands have been sent, no matter which scan method or connection
       * type we're using; now just read the result, independent of method. */

      /* Read the result */
      memset(av_buffer, 0, sizeof(av_buffer));
      bread = ip_recv(sock, av_buffer, sizeof(av_buffer), MALWARE_TIMEOUT);
      (void)close(sock);

      if (!(bread  > 0)) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: clamd: unable to read from socket (%s)",
                  strerror(errno));
        return DEFER;
      }

      if (bread == sizeof(av_buffer)) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: clamd: buffer too small");
        return DEFER;
      }

      /* Check the result. ClamAV returns one of two result formats.
      In the basic mode, the response is of the form:
        infected: -> "<filename>: <virusname> FOUND"
        not-infected: -> "<filename>: OK"
        error: -> "<filename>: <errcode> ERROR
      If the ExtendedDetectionInfo option has been turned on, then we get:
        "<filename>: <virusname>(<virushash>:<virussize>) FOUND"
      for the infected case.  Compare:
/tmp/eicar.com: Eicar-Test-Signature FOUND
/tmp/eicar.com: Eicar-Test-Signature(44d88612fea8a8f36de82e1278abb02f:68) FOUND

      In the streaming case, clamd uses the filename "stream" which you should
      be able to verify with { ktrace clamdscan --stream /tmp/eicar.com }.  (The
      client app will replace "stream" with the original filename before returning
      results to stdout, but the trace shows the data).

      We will assume that the pathname passed to clamd from Exim does not contain
      a colon.  We will have whined loudly above if the eml_filename does (and we're
      passing a filename to clamd). */

      if (!(*av_buffer)) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: clamd: ClamAV returned null");
        return DEFER;
      }

      /* strip newline at the end (won't be present for zINSTREAM)
      (also any trailing whitespace, which shouldn't exist, but we depend upon
      this below, so double-check) */
      p = av_buffer + Ustrlen(av_buffer) - 1;
      if (*p == '\n') *p = '\0';

      DEBUG(D_acl) debug_printf("Malware response: %s\n", av_buffer);

      while (isspace(*--p) && (p > av_buffer))
	*p = '\0';
      if (*p) ++p;
      response_end = p;

      /* colon in returned output? */
      if((p = Ustrchr(av_buffer,':')) == NULL) {
        log_write(0, LOG_MAIN|LOG_PANIC,
                  "malware acl condition: clamd: ClamAV returned malformed result (missing colon): %s",
                  av_buffer);
        return DEFER;
      }

      /* strip filename */
      while (*p && isspace(*++p)) /**/;
      vname = p;

      /* It would be bad to encounter a virus with "FOUND" in part of the name,
      but we should at least be resistant to it. */
      p = Ustrrchr(vname, ' ');
      if (p)
	result_tag = p + 1;
      else
	result_tag = vname;

      if (Ustrcmp(result_tag, "FOUND") == 0) {
	/* p should still be the whitespace before the result_tag */
	while (isspace(*p)) --p;
	*++p = '\0';
        /* Strip off the extended information too, which will be in parens
        after the virus name, with no intervening whitespace. */
	if (*--p == ')') {
	  /* "(hash:size)", so previous '(' will do; if not found, we have
	  a curious virus name, but not an error. */
	  p = Ustrrchr(vname, '(');
	  if (p)
	    *p = '\0';
	}
	Ustrncpy(malware_name_buffer, vname, sizeof(malware_name_buffer)-1);
	malware_name = malware_name_buffer;
	DEBUG(D_acl) debug_printf("Malware found, name \"%s\"\n", malware_name);

      } else if (Ustrcmp(result_tag, "ERROR") == 0) {
	log_write(0, LOG_MAIN|LOG_PANIC,
		  "malware acl condition: clamd: ClamAV returned: %s",
		  av_buffer);
	return DEFER;

      } else if (Ustrcmp(result_tag, "OK") == 0) {
	/* Everything should be OK */
	malware_name = NULL;
	DEBUG(D_acl) debug_printf("Malware not found\n");

      } else {
	log_write(0, LOG_MAIN|LOG_PANIC,
		  "malware acl condition: clamd: unparseable response from ClamAV: {%s}",
		  av_buffer);
	return DEFER;
      }

    } /* clamd */

    /* ----------------------------------------------------------------------- */


    /* "mksd" scanner type --------------------------------------------------- */
    else if (strcmpic(scanner_name,US"mksd") == 0) {
      uschar *mksd_options;
      char *mksd_options_end;
      uschar mksd_options_buffer[32];
      int mksd_maxproc = 1;  /* default, if no option supplied */
      struct sockaddr_un server;
      int sock;
      int retval;

      if ((mksd_options = string_nextinlist(&av_scanner_work, &sep,
                                            mksd_options_buffer,
                                            sizeof(mksd_options_buffer))) != NULL) {
        mksd_maxproc = (int) strtol(CS mksd_options, &mksd_options_end, 10);
        if ((*mksd_options == '\0') || (*mksd_options_end != '\0') ||
      (mksd_maxproc < 1) || (mksd_maxproc > 32)) {
          log_write(0, LOG_MAIN|LOG_PANIC,
                    "malware acl condition: mksd: invalid option '%s'", mksd_options);
          return DEFER;
        }
      }

      /* open the mksd socket */
      sock = socket(AF_UNIX, SOCK_STREAM, 0);
      if (sock < 0) {
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: can't open UNIX socket.");
        return DEFER;
      }
      server.sun_family = AF_UNIX;
      Ustrcpy(server.sun_path, "/var/run/mksd/socket");
      if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
        (void)close(sock);
        log_write(0, LOG_MAIN|LOG_PANIC,
             "malware acl condition: unable to connect to mksd UNIX socket (/var/run/mksd/socket). errno=%d", errno);
        return DEFER;
      }

      malware_name = NULL;

      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan\n", scanner_name);

      retval = mksd_scan_packed(sock, eml_filename);

      if (retval != OK)
        return retval;
    }
    /* ----------------------------------------------------------------------- */

    /* "unknown" scanner type ------------------------------------------------- */
    else {
      log_write(0, LOG_MAIN|LOG_PANIC,
             "malware condition: unknown scanner type '%s'", scanner_name);
      return DEFER;
    };
    /* ----------------------------------------------------------------------- */

    /* set "been here, done that" marker */
    malware_ok = 1;
  };

  /* match virus name against pattern (caseless ------->----------v) */
  if ( (malware_name != NULL) &&
       (regex_match_and_setup(re, malware_name, 0, -1)) ) {
    DEBUG(D_acl) debug_printf("Matched regex to malware [%s] [%s]\n", malware_regex, malware_name);
    return OK;
  }
  else {
    return FAIL;
  };
}


/* simple wrapper for reading lines from sockets */
int recv_line(int sock, uschar *buffer, int size) {
  uschar *p = buffer;

  memset(buffer,0,size);
  /* read until \n */
  while(recv(sock,p,1,0) > -1) {
    if ((p-buffer) > (size-2)) break;
    if (*p == '\n') break;
    if (*p != '\r') p++;
  };
  *p = '\0';

  return (p-buffer);
}


/* ============= private routines for the "mksd" scanner type ============== */

#include <sys/uio.h>

static int mksd_writev (int sock, struct iovec *iov, int iovcnt)
{
  int i;

  for (;;) {
    do
      i = writev (sock, iov, iovcnt);
    while ((i < 0) && (errno == EINTR));
    if (i <= 0) {
      close (sock);
      log_write(0, LOG_MAIN|LOG_PANIC,
                "malware acl condition: unable to write to mksd UNIX socket (/var/run/mksd/socket)");
      return -1;
    }

    for (;;)
      if (i >= iov->iov_len) {
        if (--iovcnt == 0)
          return 0;
        i -= iov->iov_len;
        iov++;
      } else {
        iov->iov_len -= i;
        iov->iov_base = CS iov->iov_base + i;
        break;
      }
  }
}

static int mksd_read_lines (int sock, uschar *av_buffer, int av_buffer_size)
{
  int offset = 0;
  int i;

  do {
    if ((i = recv (sock, av_buffer+offset, av_buffer_size-offset, 0)) <= 0) {
      close (sock);
      log_write(0, LOG_MAIN|LOG_PANIC,
                "malware acl condition: unable to read from mksd UNIX socket (/var/run/mksd/socket)");
      return -1;
    }

    offset += i;
    /* offset == av_buffer_size -> buffer full */
    if (offset == av_buffer_size) {
      close (sock);
      log_write(0, LOG_MAIN|LOG_PANIC,
                "malware acl condition: malformed reply received from mksd");
      return -1;
    }
  } while (av_buffer[offset-1] != '\n');

  av_buffer[offset] = '\0';
  return offset;
}

static int mksd_parse_line (char *line)
{
  char *p;

  switch (*line) {
    case 'O':
      /* OK */
      return OK;
    case 'E':
    case 'A':
      /* ERR */
      if ((p = strchr (line, '\n')) != NULL)
        (*p) = '\0';
      log_write(0, LOG_MAIN|LOG_PANIC,
                "malware acl condition: mksd scanner failed: %s", line);
      return DEFER;
    default:
      /* VIR */
      if ((p = strchr (line, '\n')) != NULL) {
        (*p) = '\0';
        if (((p-line) > 5) && ((p-line) < sizeof (malware_name_buffer)) && (line[3] == ' '))
          if (((p = strchr (line+4, ' ')) != NULL) && ((p-line) > 4)) {
            (*p) = '\0';
            Ustrcpy (malware_name_buffer, line+4);
      malware_name = malware_name_buffer;
            return OK;
          }
      }
      log_write(0, LOG_MAIN|LOG_PANIC,
                "malware acl condition: malformed reply received from mksd: %s", line);
      return DEFER;
  }
}

static int mksd_scan_packed(int sock, uschar *scan_filename)
{
  struct iovec iov[3];
  const char *cmd = "MSQ\n";
  uschar av_buffer[1024];

  iov[0].iov_base = (void *) cmd;
  iov[0].iov_len = 3;
  iov[1].iov_base = CS scan_filename;
  iov[1].iov_len = Ustrlen(scan_filename);
  iov[2].iov_base = (void *) cmd + 3;
  iov[2].iov_len = 1;

  if (mksd_writev (sock, iov, 3) < 0)
    return DEFER;

  if (mksd_read_lines (sock, av_buffer, sizeof (av_buffer)) < 0)
    return DEFER;

  close (sock);

  return mksd_parse_line (CS av_buffer);
}

#endif
