tidying
[exim.git] / src / src / malware.c
index 9dd241b8c84f0d62cf9c9aff7be8992ec3dbb10e..7ae8200ae952b468c085b7ff5695692f9e2fc07f 100644 (file)
@@ -2,8 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015 */
-/* License: GPL */
+/* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
+ * License: GPL
+ * Copyright (c) The Exim Maintainers 2017
+ */
 
 /* Code for calling virus (malware) scanners. Called from acl.c. */
 
@@ -11,7 +13,7 @@
 #ifdef WITH_CONTENT_SCAN
 
 typedef enum {M_FPROTD, M_DRWEB, M_AVES, M_FSEC, M_KAVD, M_CMDL,
-               M_SOPHIE, M_CLAMD, M_SOCK, M_MKSD, M_AVAST} scanner_t;
+               M_SOPHIE, M_CLAMD, M_SOCK, M_MKSD, M_AVAST, M_FPROT6D} scanner_t;
 typedef enum {MC_NONE, MC_TCP, MC_UNIX, MC_STRM} contype_t;
 static struct scan
 {
@@ -32,6 +34,7 @@ static struct scan
   { M_SOCK,    US"sock",       US"/tmp/malware.sock",                MC_STRM },
   { M_MKSD,    US"mksd",       NULL,                                 MC_NONE },
   { M_AVAST,   US"avast",      US"/var/run/avast/scan.sock",         MC_STRM },
+  { M_FPROT6D, US"f-prot6d",   US"localhost 10200",                  MC_TCP },
   { -1,                NULL,           NULL, MC_NONE }         /* end-marker */
 };
 
@@ -82,6 +85,11 @@ static const uschar * ava_re_virus_str = US "(?!\\\\)\\t\\[L\\]\\d\\.\\d\\t\\d\\
 static const pcre * ava_re_clean = NULL;
 static const pcre * ava_re_virus = NULL;
 
+static const uschar * fprot6d_re_error_str = US "^\\d+\\s<(.+?)>$";
+static const uschar * fprot6d_re_virus_str = US "^\\d+\\s<infected:\\s+(.+?)>\\s+.+$";
+static const pcre * fprot6d_re_error = NULL;
+static const pcre * fprot6d_re_virus = NULL;
+
 
 
 /******************************************************************************/
@@ -96,7 +104,7 @@ static inline int
 test_byte_order()
 {
   short int word = 0x0001;
-  char *byte = (char *) &word;
+  char *byte = CS  &word;
   return(byte[0] ? LITTLE_MY_ENDIAN : BIG_MY_ENDIAN);
 }
 
@@ -106,7 +114,7 @@ BOOL malware_ok = FALSE;
 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];
+extern uschar spooled_message_id[MESSAGE_ID_LENGTH+1];
 
 
 
@@ -139,9 +147,10 @@ uses the returned in_addr to get a second connection to the same system.
 */
 static inline int
 m_tcpsocket(const uschar * hostname, unsigned int port,
-       host_item * host, uschar ** errstr)
+       host_item * host, uschar ** errstr, const blob * fastopen_blob)
 {
-return ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5, host, errstr);
+return ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5,
+                         host, errstr, fastopen_blob);
 }
 
 static int
@@ -194,7 +203,11 @@ const pcre * cre = NULL;
 if (!(list_ele = string_nextinlist(list, sep, NULL, 0)))
   *errstr = US listerr;
 else
+  {
+  DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "RE: ",
+    string_printing(list_ele));
   cre = m_pcre_compile(CUS list_ele, errstr);
+  }
 return cre;
 }
 
@@ -229,13 +242,13 @@ while ((rcv = read(fd, p, 1)) > 0)
   }
 if (!ok)
   {
-  DEBUG(D_acl) debug_printf("Malware scan: read %s (%s)\n",
+  DEBUG(D_acl) debug_printf_indent("Malware scan: read %s (%s)\n",
                rcv==0 ? "EOF" : "error", strerror(errno));
   return rcv==0 ? -1 : -2;
   }
 *p = '\0';
 
-DEBUG(D_acl) debug_printf("Malware scan: read '%s'\n", buffer);
+DEBUG(D_acl) debug_printf_indent("Malware scan: read '%s'\n", buffer);
 return p - buffer;
 }
 
@@ -405,16 +418,16 @@ is via malware(), or there's malware_in_file() used for testing/debugging.
 
 Arguments:
   malware_re    match condition for "malware="
-  eml_filename  the file holding the email to be scanned
+  scan_filename  the file holding the email to be scanned, if we're faking
+               this up for the -bmalware test, else NULL
   timeout      if nonzero, non-default timeoutl
-  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(const uschar * malware_re, const uschar * eml_filename,
-  int timeout, BOOL faking)
+malware_internal(const uschar * malware_re, const uschar * scan_filename,
+  int timeout)
 {
 int sep = 0;
 const uschar *av_scanner_work = av_scanner;
@@ -427,21 +440,24 @@ struct scan * scanent;
 const uschar * scanner_options;
 int sock = -1;
 time_t tmo;
+uschar * eml_filename, * eml_dir;
 
-/* make sure the eml mbox file is spooled up */
-if (!(mbox_file = spool_mbox(&mbox_size, faking ? eml_filename : NULL)))
+if (!malware_re)
+  return FAIL;         /* empty means "don't match anything" */
+
+/* Ensure the eml mbox file is spooled up */
+
+if (!(mbox_file = spool_mbox(&mbox_size, scan_filename, &eml_filename)))
   return malware_errlog_defer(US"error while creating mbox spool file");
 
-/* none of our current scanners need the mbox
-   file as a stream, so we can close it right away */
-(void)fclose(mbox_file);
+/* None of our current scanners need the mbox file as a stream (they use
+the name), so we can close it right away.  Get the directory too. */
 
-if (!malware_re)
-  return FAIL;         /* empty means "don't match anything" */
+(void) fclose(mbox_file);
+eml_dir = string_copyn(eml_filename, Ustrrchr(eml_filename, '/') - eml_filename);
 
 /* parse 1st option */
-  if ( (strcmpic(malware_re, US"false") == 0) ||
-     (Ustrcmp(malware_re,"0") == 0) )
+if (strcmpic(malware_re, US"false") == 0  ||  Ustrcmp(malware_re,"0") == 0)
   return FAIL;         /* explicitly no matching */
 
 /* special cases (match anything except empty) */
@@ -461,9 +477,6 @@ if (  strcmpic(malware_re,US"true") == 0
 else if (!(re = m_pcre_compile(malware_re, &errstr)))
   return malware_errlog_defer(errstr);
 
-/* Reset sep that is set by previous string_nextinlist() call */
-sep = 0;
-
 /* if av_scanner starts with a dollar, expand it first */
 if (*av_scanner == '$')
   {
@@ -473,7 +486,7 @@ if (*av_scanner == '$')
         expand_string_message));
 
   DEBUG(D_acl)
-    debug_printf("Expanded av_scanner global: %s\n", av_scanner_work);
+    debug_printf_indent("Expanded av_scanner global: %s\n", av_scanner_work);
   /* disable result caching in this case */
   malware_name = NULL;
   malware_ok = FALSE;
@@ -495,14 +508,19 @@ if (!malware_ok)
        scanner_name));
     if (strcmpic(scanner_name, US scanent->name) != 0)
       continue;
+    DEBUG(D_acl) debug_printf_indent("Malware scan:  %s tmo=%s\n",
+      scanner_name, readconf_printtime(timeout));
+
     if (!(scanner_options = string_nextinlist(&av_scanner_work, &sep, NULL, 0)))
       scanner_options = scanent->options_default;
     if (scanent->conn == MC_NONE)
       break;
+
+    DEBUG(D_acl) debug_printf_indent("%15s%10s%s\n", "", "socket: ", scanner_options);
     switch(scanent->conn)
     {
-    case MC_TCP:  sock = ip_tcpsocket(scanner_options, &errstr, 5);      break;
-    case MC_UNIX: sock = ip_unixsocket(scanner_options, &errstr);        break;
+    case MC_TCP:  sock = ip_tcpsocket(scanner_options, &errstr, 5);    break;
+    case MC_UNIX: sock = ip_unixsocket(scanner_options, &errstr);      break;
     case MC_STRM: sock = ip_streamsocket(scanner_options, &errstr, 5);  break;
     default: /* compiler quietening */ break;
     }
@@ -510,7 +528,6 @@ if (!malware_ok)
       return m_errlog_defer(scanent, CUS callout_address, errstr);
     break;
   }
-  DEBUG(D_acl) debug_printf("Malware scan: %s tmo %s\n", scanner_name, readconf_printtime(timeout));
 
   switch (scanent->scancode)
     {
@@ -533,7 +550,7 @@ if (!malware_ok)
        par_count++;
        }
       scanrequest = string_sprintf("%s HTTP/1.0\r\n\r\n", scanrequest);
-      DEBUG(D_acl) debug_printf("Malware scan: issuing %s: %s\n",
+      DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s: %s\n",
        scanner_name, scanrequest);
 
       /* send scan request */
@@ -594,7 +611,8 @@ if (!malware_ok)
 
        if ((fsize = lseek(drweb_fd, 0, SEEK_END)) == -1)
          {
-         int err = errno;
+         int err;
+badseek:  err = errno;
          (void)close(drweb_fd);
          return m_errlog_defer_3(scanent, NULL,
            string_sprintf("can't seek spool file %s: %s",
@@ -611,9 +629,10 @@ if (!malware_ok)
            sock);
          }
        drweb_slen = htonl(fsize);
-       lseek(drweb_fd, 0, SEEK_SET);
+       if (lseek(drweb_fd, 0, SEEK_SET) < 0)
+         goto badseek;
 
-       DEBUG(D_acl) debug_printf("Malware scan: issuing %s remote scan [%s]\n",
+       DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s remote scan [%s]\n",
            scanner_name, scanner_options);
 
        /* send scan request */
@@ -628,7 +647,7 @@ if (!malware_ok)
            sock);
          }
 
-       if (!(drweb_fbuf = (uschar *) malloc (fsize_uint)))
+       if (!(drweb_fbuf = US malloc(fsize_uint)))
          {
          (void)close(drweb_fd);
          return m_errlog_defer_3(scanent, NULL,
@@ -657,13 +676,12 @@ if (!malware_ok)
            "unable to send file body to socket (%s)", scanner_options),
            sock);
          }
-       (void)close(drweb_fd);
        }
       else
        {
        drweb_slen = htonl(Ustrlen(eml_filename));
 
-       DEBUG(D_acl) debug_printf("Malware scan: issuing %s local scan [%s]\n",
+       DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s local scan [%s]\n",
            scanner_name, scanner_options);
 
        /* send scan request */
@@ -692,6 +710,7 @@ if (!malware_ok)
       if (drweb_vnum)
        {
        int i;
+       gstring * g = NULL;
 
        /* setup default virus name */
        malware_name = US"unknown";
@@ -703,7 +722,8 @@ if (!malware_ok)
        /* read and concatenate virus names into one string */
        for (i = 0; i < drweb_vnum; i++)
          {
-         int size = 0, off = 0, ovector[10*3];
+         int ovector[10*3];
+
          /* read the size of report */
          if (!recv_len(sock, &drweb_slen, sizeof(drweb_slen), tmo))
            return m_errlog_defer_3(scanent, CUS callout_address,
@@ -727,16 +747,16 @@ if (!malware_ok)
            pcre_get_substring(CS tmpbuf, ovector, result, 1, &pre_malware_nb);
 
            if (i==0)   /* the first name we just copy to malware_name */
-             malware_name = string_append(NULL, &size, &off,
-                                         1, pre_malware_nb);
+             g = string_cat(NULL, US pre_malware_nb);
 
+           /*XXX could be string_append_listele? */
            else        /* concatenate each new virus name to previous */
-             malware_name = string_append(malware_name, &size, &off,
-                                         2, "/", pre_malware_nb);
+             g = string_append(g, 2, "/", pre_malware_nb);
 
            pcre_free_substring(pre_malware_nb);
            }
          }
+         malware_name = string_from_gstring(g);
        }
       else
        {
@@ -773,7 +793,7 @@ if (!malware_ok)
       if (buf[0] != '2')               /* aveserver is having problems */
        return m_errlog_defer_3(scanent, CUS callout_address,
          string_sprintf("unavailable (Responded: %s).",
-                         ((buf[0] != 0) ? buf : (uschar *)"nothing") ),
+                         ((buf[0] != 0) ? buf : US "nothing") ),
          sock);
 
       /* prepare our command */
@@ -781,7 +801,7 @@ if (!malware_ok)
                                                eml_filename);
 
       /* and send it */
-      DEBUG(D_acl) debug_printf("Malware scan: issuing %s %s\n",
+      DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s %s\n",
        scanner_name, buf);
       if (m_sock_send(sock, buf, Ustrlen(buf), &errstr) < 0)
        return m_errlog_defer(scanent, CUS callout_address, errstr);
@@ -818,7 +838,7 @@ if (!malware_ok)
       if (buf[0] != '2')               /* aveserver is having problems */
        return m_errlog_defer_3(scanent, CUS callout_address,
          string_sprintf("unable to quit dialogue (Responded: %s).",
-                       ((buf[0] != 0) ? buf : (uschar *)"nothing") ),
+                       ((buf[0] != 0) ? buf : US "nothing") ),
          sock);
 
       if (result == DEFER)
@@ -841,7 +861,7 @@ if (!malware_ok)
 
       malware_name = NULL;
 
-      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
+      DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n",
          scanner_name, scanner_options);
       /* pass options */
       memset(av_buffer, 0, sizeof(av_buffer));
@@ -937,7 +957,7 @@ if (!malware_ok)
       if (p)
        *p = '\0';
 
-      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
+      DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n",
          scanner_name, scanner_options);
 
       /* send scan request */
@@ -965,7 +985,7 @@ if (!malware_ok)
                US"reported 'kavdaemon damaged' (code 7).", sock);
       }
 
-      /* code 8 is not handled, since it is ambigous. It appears mostly on
+      /* code 8 is not handled, since it is ambiguous. It appears mostly on
       bounces where part of a file has been cut off */
 
       /* "virus found" return codes (2-4) */
@@ -1002,7 +1022,9 @@ if (!malware_ok)
              kav_re = kav_re_inf;
              }
 
-           /* read report, linewise */
+           /* read report, linewise.  Using size from stream to read amount of data
+           from same stream is safe enough. */
+           /* coverity[tainted_data] */
            while (kav_reportlen > 0)
              {
              if ((bread = recv_line(sock, tmpbuf, sizeof(tmpbuf), tmo)) < 0)
@@ -1066,7 +1088,7 @@ if (!malware_ok)
       /* redirect STDERR too */
       commandline = string_sprintf("%s 2>&1", commandline);
 
-      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
+      DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n",
              scanner_name, commandline);
 
       /* store exims signal handlers */
@@ -1082,8 +1104,7 @@ if (!malware_ok)
        }
       scanner_fd = fileno(scanner_out);
 
-      file_name = string_sprintf("%s/scan/%s/%s_scanner_output",
-                               spool_directory, message_id, message_id);
+      file_name = string_sprintf("%s/%s_scanner_output", eml_dir, message_id);
 
       if (!(scanner_record = modefopen(file_name, "wb", SPOOL_MODE)))
        {
@@ -1169,7 +1190,7 @@ if (!malware_ok)
       if ((p = Ustrrchr(file_name, '/')))
        *p = '\0';
 
-      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n",
+      DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n",
          scanner_name, scanner_options);
 
       if (  write(sock, file_name, Ustrlen(file_name)) < 0
@@ -1218,7 +1239,6 @@ if (!malware_ok)
 
       uschar *p, *vname, *result_tag;
       int bread=0;
-      uschar * file_name;
       uschar av_buffer[1024];
       uschar *hostname = US"";
       host_item connhost;
@@ -1236,6 +1256,7 @@ if (!malware_ok)
 #else
       uint32_t send_size, send_final_zeroblock;
 #endif
+      blob cmd_str;
 
       /*XXX if unixdomain socket, only one server supported. Needs fixing;
       there's no reason we should not mix local and remote servers */
@@ -1304,11 +1325,8 @@ if (!malware_ok)
          /* parse options */
          /*XXX should these options be common over scanner types? */
          if (clamd_option(cd, sublist, &subsep) != OK)
-           {
            return m_errlog_defer(scanent, NULL,
              string_sprintf("bad option '%s'", scanner_options));
-           continue;
-           }
 
          cv[num_servers++] = cd;
          if (num_servers >= MAX_CLAMD_SERVERS)
@@ -1334,6 +1352,19 @@ if (!malware_ok)
          string_sprintf("local/SCAN mode incompatible with" \
            " : in path to email filename [%s]", eml_filename));
 
+      /* Set up the very first data we will be sending */
+      if (!use_scan_command)
+#ifdef WITH_OLD_CLAMAV_STREAM
+       { cmd_str.data = US"STREAM\n"; cmd_str.len = 7; }
+#else
+       { cmd_str.data = US"zINSTREAM"; cmd_str.len = 10; }
+#endif
+      else
+       {
+       cmd_str.data = string_sprintf("SCAN %s\n", eml_filename);
+       cmd_str.len = Ustrlen(cmd_str.data);
+       }
+
       /* We have some network servers specified */
       if (num_servers)
        {
@@ -1343,21 +1374,22 @@ if (!malware_ok)
 
        while (num_servers > 0)
          {
-         int i = random_number( num_servers );
+         int i = random_number(num_servers);
          clamd_address * cd = cv[i];
 
-         DEBUG(D_acl) debug_printf("trying server name %s, port %u\n",
+         DEBUG(D_acl) debug_printf_indent("trying server name %s, port %u\n",
                         cd->hostspec, cd->tcp_port);
 
          /* Lookup the host. This is to ensure that we connect to the same IP
           * on both connections (as one host could resolve to multiple ips) */
          for (;;)
            {
-           sock= m_tcpsocket(cd->hostspec, cd->tcp_port, &connhost, &errstr);
-           if (sock >= 0)
+           if ((sock = m_tcpsocket(cd->hostspec, cd->tcp_port,
+                                   &connhost, &errstr, &cmd_str)) >= 0)
              {
              /* Connection successfully established with a server */
              hostname = cd->hostspec;
+             cmd_str.len = 0;
              break;
              }
            if (cd->retry <= 0) break;
@@ -1402,13 +1434,14 @@ if (!malware_ok)
         * 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(
+       DEBUG(D_acl) debug_printf_indent(
            "Malware scan: issuing %s old-style remote scan (PORT)\n",
            scanner_name);
 
-       /* Pass the string to ClamAV (7 = "STREAM\n") */
-       if (m_sock_send(sock, US"STREAM\n", 7, &errstr) < 0)
-         return m_errlog_defer(scanent, CUS callout_address, errstr);
+       /* Pass the string to ClamAV (7 = "STREAM\n"), if not already sent */
+       if (cmd_str.len)
+         if (m_sock_send(sock, cmd_str.data, cmd_str.len, &errstr) < 0)
+           return m_errlog_defer(scanent, CUS callout_address, errstr);
 
        memset(av_buffer2, 0, sizeof(av_buffer2));
        bread = ip_recv(sock, av_buffer2, sizeof(av_buffer2), tmo-time(NULL));
@@ -1428,13 +1461,13 @@ if (!malware_ok)
                  "ClamAV returned null", sock);
 
        av_buffer2[bread] = '\0';
-       if( sscanf(CS av_buffer2, "PORT %u\n", &port) != 1 )
+       if(sscanf(CS av_buffer2, "PORT %u\n", &port) != 1)
          return m_errlog_defer_3(scanent, CUS callout_address,
            string_sprintf("Expected port information from clamd, got '%s'",
              av_buffer2),
            sock);
 
-       sockData = m_tcpsocket(connhost.address, port, NULL, &errstr);
+       sockData = m_tcpsocket(connhost.address, port, NULL, &errstr, NULL);
        if (sockData < 0)
          return m_errlog_defer_3(scanent, CUS callout_address, errstr, sock);
 
@@ -1444,16 +1477,17 @@ if (!malware_ok)
        chunks, <n> a 4-byte number (network order), terminated by a zero-length
        chunk. */
 
-       DEBUG(D_acl) debug_printf(
+       DEBUG(D_acl) debug_printf_indent(
            "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)
-         return m_errlog_defer_3(scanent, CUS hostname,
-           string_sprintf("unable to send zINSTREAM to socket (%s)",
-             strerror(errno)),
-           sock);
+       /* Pass the string to ClamAV (10 = "zINSTREAM\0"), if not already sent */
+       if (cmd_str.len)
+         if (send(sock, cmd_str.data, cmd_str.len, 0) < 0)
+           return m_errlog_defer_3(scanent, CUS hostname,
+             string_sprintf("unable to send zINSTREAM to socket (%s)",
+               strerror(errno)),
+             sock);
 
 # define CLOSE_SOCKDATA /**/
 #endif
@@ -1470,7 +1504,8 @@ if (!malware_ok)
          }
        if ((fsize = lseek(clam_fd, 0, SEEK_END)) < 0)
          {
-         int err = errno;
+         int err;
+b_seek:   err = errno;
          CLOSE_SOCKDATA; (void)close(clam_fd);
          return m_errlog_defer_3(scanent, NULL,
            string_sprintf("can't seek spool file %s: %s",
@@ -1486,9 +1521,10 @@ if (!malware_ok)
              eml_filename),
            sock);
          }
-       lseek(clam_fd, 0, SEEK_SET);
+       if (lseek(clam_fd, 0, SEEK_SET) < 0)
+         goto b_seek;
 
-       if (!(clamav_fbuf = (uschar *) malloc (fsize_uint)))
+       if (!(clamav_fbuf = US malloc(fsize_uint)))
          {
          CLOSE_SOCKDATA; (void)close(clam_fd);
          return m_errlog_defer_3(scanent, NULL,
@@ -1552,17 +1588,17 @@ if (!malware_ok)
        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) */
-       file_name = string_sprintf("SCAN %s\n", eml_filename);
+       /* Pass the string to ClamAV (7 = "SCAN \n" + \0), if not already sent */
 
-       DEBUG(D_acl) debug_printf(
+       DEBUG(D_acl) debug_printf_indent(
            "Malware scan: issuing %s local-path scan [%s]\n",
            scanner_name, scanner_options);
 
-       if (send(sock, file_name, Ustrlen(file_name), 0) < 0)
-         return m_errlog_defer_3(scanent, CUS callout_address,
-           string_sprintf("unable to write to socket (%s)", strerror(errno)),
-           sock);
+       if (cmd_str.len)
+         if (send(sock, cmd_str.data, cmd_str.len, 0) < 0)
+           return m_errlog_defer_3(scanent, CUS callout_address,
+             string_sprintf("unable to write to socket (%s)", strerror(errno)),
+             sock);
 
        /* Do not shut down the socket for writing; a user report noted that
         * clamd 0.70 does not react well to this. */
@@ -1616,7 +1652,7 @@ if (!malware_ok)
       p = av_buffer + Ustrlen(av_buffer) - 1;
       if (*p == '\n') *p = '\0';
 
-      DEBUG(D_acl) debug_printf("Malware response: %s\n", av_buffer);
+      DEBUG(D_acl) debug_printf_indent("Malware response: %s\n", av_buffer);
 
       while (isspace(*--p) && (p > av_buffer))
        *p = '\0';
@@ -1653,7 +1689,7 @@ if (!malware_ok)
            *p = '\0';
          }
        malware_name = string_copy(vname);
-       DEBUG(D_acl) debug_printf("Malware found, name \"%s\"\n", malware_name);
+       DEBUG(D_acl) debug_printf_indent("Malware found, name \"%s\"\n", malware_name);
 
        }
       else if (Ustrcmp(result_tag, "ERROR") == 0)
@@ -1664,7 +1700,7 @@ if (!malware_ok)
        {
        /* Everything should be OK */
        malware_name = NULL;
-       DEBUG(D_acl) debug_printf("Malware not found\n");
+       DEBUG(D_acl) debug_printf_indent("Malware not found\n");
 
        }
       else
@@ -1689,8 +1725,10 @@ if (!malware_ok)
       const pcre *sockline_name_re;
 
       /* find scanner command line */
-      if ((sockline_scanner = string_nextinlist(&av_scanner_work, &sep,
-                                         NULL, 0)))
+      if (  (sockline_scanner = string_nextinlist(&av_scanner_work, &sep,
+                                         NULL, 0))
+        && *sockline_scanner
+        )
       {        /* check for no expansions apart from one %s */
        uschar * s = Ustrchr(sockline_scanner, '%');
        if (s++)
@@ -1700,6 +1738,8 @@ if (!malware_ok)
       }
       else
        sockline_scanner = sockline_scanner_default;
+      DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "cmdline: ",
+       string_printing(sockline_scanner));
 
       /* find scanner output trigger */
       sockline_trig_re = m_pcre_nextinlist(&av_scanner_work, &sep,
@@ -1714,9 +1754,9 @@ if (!malware_ok)
        return m_errlog_defer_3(scanent, NULL, errstr, sock);
 
       /* prepare scanner call - security depends on expansions check above */
-      commandline = string_sprintf("%s/scan/%s/%s.eml", spool_directory, message_id, message_id);
-      commandline = string_sprintf( CS sockline_scanner, CS commandline);
-
+      commandline = string_sprintf( CS sockline_scanner, CS eml_filename);
+      DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "expanded: ",
+       string_printing(commandline));
 
       /* Pass the command string to the socket */
       if (m_sock_send(sock, commandline, Ustrlen(commandline), &errstr) < 0)
@@ -1735,12 +1775,16 @@ if (!malware_ok)
                US"buffer too small", sock);
       av_buffer[bread] = '\0';
       linebuffer = string_copy(av_buffer);
+      DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "answer: ",
+       string_printing(linebuffer));
 
       /* try trigger match */
       if (regex_match_and_setup(sockline_trig_re, linebuffer, 0, -1))
        {
        if (!(malware_name = m_pcre_exec(sockline_name_re, av_buffer)))
          malware_name = US "unknown";
+       DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "name: ",
+         string_printing(malware_name));
        }
       else /* no virus found */
        malware_name = NULL;
@@ -1770,7 +1814,7 @@ if (!malware_ok)
 
       malware_name = NULL;
 
-      DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan\n", scanner_name);
+      DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan\n", scanner_name);
 
       if ((retval = mksd_scan_packed(scanent, sock, eml_filename, tmo)) != OK)
        {
@@ -1812,7 +1856,7 @@ if (!malware_ok)
        int slen = Ustrlen(buf);
        if (slen >= 1)
          {
-         DEBUG(D_acl) debug_printf("got from avast: %s\n", buf);
+         DEBUG(D_acl) debug_printf_indent("got from avast: %s\n", buf);
          switch (avast_stage)
            {
            case AVA_HELO:
@@ -1838,8 +1882,7 @@ if (!malware_ok)
                }
              else
                {
-               scanrequest = string_sprintf("SCAN %s/scan/%s\n",
-                   spool_directory, message_id);
+               scanrequest = string_sprintf("SCAN %s\n", eml_dir);
                avast_stage = AVA_RSP;          /* just sent command */
                }
 
@@ -1911,8 +1954,53 @@ if (!malware_ok)
                          sock);
        default:        break;
        }
+      break;
       }
+
+    case M_FPROT6D: /* "f-prot6d" scanner type ----------------------------------- */
+      {
+      int bread;
+      uschar * e;
+      uschar * linebuffer;
+      uschar * scanrequest;
+      uschar av_buffer[1024];
+
+      if ((!fprot6d_re_virus && !(fprot6d_re_virus = m_pcre_compile(fprot6d_re_virus_str, &errstr)))
+        || (!fprot6d_re_error && !(fprot6d_re_error = m_pcre_compile(fprot6d_re_error_str, &errstr))))
+        return malware_errlog_defer(errstr);
+
+      scanrequest = string_sprintf("SCAN FILE %s\n", eml_filename);
+      DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s: %s\n",
+        scanner_name, scanrequest);
+
+      if (m_sock_send(sock, scanrequest, Ustrlen(scanrequest), &errstr) < 0)
+        return m_errlog_defer(scanent, CUS callout_address, errstr);
+
+      bread = ip_recv(sock, av_buffer, sizeof(av_buffer), tmo-time(NULL));
+
+      if (bread <= 0)
+        return m_errlog_defer_3(scanent, CUS callout_address,
+          string_sprintf("unable to read from socket (%s)", strerror(errno)),
+          sock);
+
+      if (bread == sizeof(av_buffer))
+        return m_errlog_defer_3(scanent, CUS callout_address,
+          US"buffer too small", sock);
+
+      av_buffer[bread] = '\0';
+      linebuffer = string_copy(av_buffer);
+
+      m_sock_send(sock, US"QUIT\n", 5, 0);
+
+      if ((e = m_pcre_exec(fprot6d_re_error, linebuffer)))
+        return m_errlog_defer_3(scanent, CUS callout_address,
+          string_sprintf("scanner reported error (%s)", e), sock);
+
+      if (!(malware_name = m_pcre_exec(fprot6d_re_virus, linebuffer)))
+        malware_name = NULL;
+
       break;
+      }  /* f-prot6d */
   }    /* scanner type switch */
 
   if (sock >= 0)
@@ -1923,7 +2011,7 @@ if (!malware_ok)
 /* match virus name against pattern (caseless ------->----------v) */
 if (malware_name && regex_match_and_setup(re, malware_name, 0, -1))
   {
-  DEBUG(D_acl) debug_printf(
+  DEBUG(D_acl) debug_printf_indent(
       "Matched regex to malware [%s] [%s]\n", malware_re, malware_name);
   return OK;
   }
@@ -1949,15 +2037,10 @@ Returns:      Exim message processing code (OK, FAIL, DEFER, ...)
 int
 malware(const uschar * malware_re, int timeout)
 {
-  uschar * scan_filename;
-  int ret;
+int ret = malware_internal(malware_re, NULL, timeout);
 
-  scan_filename = string_sprintf("%s/scan/%s/%s.eml",
-                   spool_directory, message_id, message_id);
-  ret = malware_internal(malware_re, scan_filename, timeout, FALSE);
-  if (ret == DEFER) av_failed = TRUE;
-
-  return ret;
+if (ret == DEFER) av_failed = TRUE;
+return ret;
 }
 
 
@@ -1979,32 +2062,35 @@ Returns:        Exim message processing code (OK, FAIL, DEFER, ...)
 int
 malware_in_file(uschar *eml_filename)
 {
-  uschar message_id_buf[64];
-  int ret;
-
-  /* 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", vaguely_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(US"*", eml_filename, 0,  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();
-
-  /* silence static analysis tools */
-  message_id = NULL;
-
-  return ret;
+uschar message_id_buf[64];
+int ret;
+
+/* 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", vaguely_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(US"*", eml_filename, 0);
+
+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();
+
+/* silence static analysis tools */
+message_id = NULL;
+
+return ret;
 }
 
 
@@ -2025,6 +2111,10 @@ if (!ava_re_clean)
   ava_re_clean = regex_must_compile(ava_re_clean_str, FALSE, TRUE);
 if (!ava_re_virus)
   ava_re_virus = regex_must_compile(ava_re_virus_str, FALSE, TRUE);
+if (!fprot6d_re_error)
+  fprot6d_re_error = regex_must_compile(fprot6d_re_error_str, FALSE, TRUE);
+if (!fprot6d_re_virus)
+  fprot6d_re_virus = regex_must_compile(fprot6d_re_virus_str, FALSE, TRUE);
 }
 
 #endif /*WITH_CONTENT_SCAN*/