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

/*
 * Copyright (c) The Exim Maintainers 2015 - 2026
 * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
 * License: GPL
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

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

#include "exim.h"
#ifdef WITH_CONTENT_SCAN	/* entire file */

typedef enum {
#ifndef DISABLE_MAL_CLAM
	M_CLAMD,
#endif
#ifndef DISABLE_MAL_AVAST
	M_AVAST,
#endif
#ifndef DISABLE_MAL_SOCK
	M_SOCK,
#endif
#ifndef DISABLE_MAL_CMDLINE
	M_CMDL,
#endif
	M_DUMMY
	} scanner_t;
typedef enum {MC_NONE, MC_TCP, MC_UNIX, MC_STRM} contype_t;
static struct scan
{
  scanner_t	scancode;
  const uschar * name;
  const uschar * options_default;
  contype_t	conn;
} m_scans[] =
{
#ifndef DISABLE_MAL_CLAM
  { M_CLAMD,	US"clamd",	US"/tmp/clamd",			      MC_NONE },
#endif
#ifndef DISABLE_MAL_AVAST
  { M_AVAST,	US"avast",	US"/var/run/avast/scan.sock",	      MC_STRM },
#endif
#ifndef DISABLE_MAL_SOCK
  { M_SOCK,	US"sock",	US"/tmp/malware.sock",		      MC_STRM },
#endif
#ifndef DISABLE_MAL_CMDLINE
  { M_CMDL,	US"cmdline",	NULL,				      MC_NONE },
#endif
  { -1,		NULL,		NULL, MC_NONE }		/* end-marker */
};

/******************************************************************************/
# ifdef MACRO_PREDEF		/* build solely to predefine macros */

#  include "macro_predef.h"

void
features_malware(void)
{
const uschar * s;
uschar * t;
uschar buf[EXIM_DRIVERNAME_MAX];

spf(buf, sizeof(buf), US"_HAVE_MALWARE_");

for (const struct scan * sc = m_scans; sc->scancode != -1; sc++)
  {
  for (s = sc->name, t = buf+14; *s; s++) if (*s != '-')
    *t++ = toupper(*s);
  *t = '\0';
  builtin_macro_create(buf);
  }
}

/******************************************************************************/
# else	/*!MACRO_PREDEF, main build*/


#define MALWARE_TIMEOUT 120	/* default timeout, seconds */

static const uschar * malware_regex_default = US ".+";
static const pcre2_code * malware_default_re = NULL;


#ifndef DISABLE_MAL_CLAM
/* The maximum number of clamd servers that are supported in the configuration */
# define MAX_CLAMD_SERVERS 32
# define MAX_CLAMD_SERVERS_S "32"

typedef struct clamd_address {
  uschar * hostspec;
  unsigned tcp_port;
  unsigned retry;
} clamd_address;
#endif


#ifndef DISABLE_MAL_AVAST
static const uschar * ava_re_clean_str = US "(?!\\\\)\\t\\[\\+\\]";
static const uschar * ava_re_virus_str = US "(?!\\\\)\\t\\[L\\]\\d+\\.0\\t0\\s(.*)";
static const uschar * ava_re_error_str = US "(?!\\\\)\\t\\[E\\]\\d+\\.0\\tError\\s\\d+\\s(.*)";
static const pcre2_code * ava_re_clean = NULL;
static const pcre2_code * ava_re_virus = NULL;
static const pcre2_code * ava_re_error = NULL;
#endif



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

BOOL malware_ok = FALSE;

/* 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[MESSAGE_ID_LENGTH+1];


/* Some (currently avast only) use backslash escaped whitespace,
this function undoes these escapes */

#ifndef DISABLE_MAL_AVAST
static inline void
unescape(uschar *p)
{
uschar *p0;
for (; *p; ++p)
  if (*p == '\\' && (isspace(p[1]) || p[1] == '\\'))
    for (p0 = p; *p0; ++p0) *p0 = p0[1];
}
#endif

/* --- malware_*_defer --- */
static inline int
malware_panic_defer(const uschar * str)
{
log_write(LOG_MAIN|LOG_PANIC, "malware acl condition: %s", str);
return DEFER;
}
/* --- m_*_defer --- */
static inline int
m_panic_defer(struct scan * scanent, const uschar * hostport,
  const uschar * str)
{
return malware_panic_defer(string_sprintf("%s %s : %s",
  scanent->name, hostport ? hostport : CUS"", str));
}
/* --- m_*_defer_3 */
static inline int
m_panic_defer_3(struct scan * scanent, const uschar * hostport,
  const uschar * str, int fd_to_close)
{
DEBUG(acl) debug_print_socket(fd_to_close);
(void) close(fd_to_close);
return m_panic_defer(scanent, hostport, str);
}

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

#ifndef DISABLE_MAL_CLAM
/* Only used by the Clamav code, which is working from a list of servers and
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, const blob * fastopen_blob)
{
int fd = ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5,
			  host, errstr, fastopen_blob);
#ifdef EXIM_TFO_FREEBSD
/* Under some fault conditions, FreeBSD 12.2 seen to send a (non-TFO) SYN
and, getting no response, wait for a long time.  Impose a 5s max. */
if (fd >= 0)
  (void) poll_one_fd(fd, POLLOUT, 5 * 1000);
#endif
return fd;
}
#endif

static int
m_sock_send(int sock, uschar * buf, int cnt, uschar ** errstr)
{
if (send(sock, buf, cnt, 0) < 0)
  {
  int err = errno;
  (void)close(sock);
  *errstr = string_sprintf("unable to send to socket (%s): %s",
	 buf, strerror(err));
  return -1;
  }
return sock;
}

static const pcre2_code *
m_pcre_compile(const uschar * re, BOOL cacheable, uschar ** errstr)
{
return regex_compile(re, cacheable ? MCS_CACHEABLE : MCS_NOFLAGS, errstr,
		      pcre_gen_cmp_ctx);
}

uschar *
m_pcre_exec(const pcre2_code * cre, uschar * text)
{
pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx);
int i = pcre2_match(cre, text, PCRE2_ZERO_TERMINATED, 0, 0, md, pcre_gen_mtc_ctx);
uschar * substr = NULL;

if (i >= 2)				/* Got it */
  {
  PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
  int len = ovec[3] - ovec[2];
  substr = string_copyn(text + ovec[2], len);
  }
/* pcre2_match_data_free(md);	gen ctx needs no free */
return substr;
}

static const pcre2_code *
m_pcre_nextinlist(const uschar ** list, int * sep,
 BOOL cacheable, char * listerr, uschar ** errstr)
{
const uschar * list_ele;
const pcre2_code * cre = NULL;

if (!(list_ele = string_nextinlist(list, sep, NULL, 0)))
  *errstr = US listerr;
else
  {
  DEBUG(acl) debug_printf_indent("%15s%10s'%s'\n", "", "RE: ",
    string_printing(list_ele));
  cre = m_pcre_compile(CUS list_ele, cacheable, errstr);
  }
return cre;
}


/*
 Simple though inefficient wrapper for reading a line.  Drop CRs and the
 trailing newline. Can return early on buffer full. Null-terminate.
 Apply initial timeout if no data ready.

 Return: number of chars - zero for an empty line
	 -1 on EOF
         -2 on timeout or error
*/
static int
recv_line(int fd, uschar * buffer, int bsize, time_t tmo)
{
uschar * p = buffer;
ssize_t rcv;
BOOL ok = FALSE;

*buffer = '\0';

if (!fd_ready(fd, tmo))
  return -2;

/*XXX tmo handling assumes we always get a whole line */
/* read until \n */
errno = 0;
while ((rcv = read(fd, p, 1)) > 0)
  {
  ok = TRUE;
  if (p-buffer > bsize-2) break;
  if (*p == '\n') break;
  if (*p != '\r') p++;
  }
if (!ok)
  {
  DEBUG(acl)
    {
    debug_printf_indent("Malware scan: read %s (%s)\n",
		rcv==0 ? "EOF" : "error", strerror(errno));
    debug_print_socket(fd);
    }
  return rcv==0 ? -1 : -2;
  }
*p = '\0';

DEBUG(acl) debug_printf_indent("Malware scan: read '%s'\n", buffer);
return p - buffer;
}

#ifndef DISABLE_MAL_CLAM
static int
clamd_option(clamd_address * cd, const uschar * optstr, int * subsep)
{
uschar * s;

cd->retry = 0;
while ((s = string_nextinlist(&optstr, subsep, NULL, 0)))
  if (Ustrncmp(s, "retry=", 6) == 0)
    {
    int sec = readconf_readtime((s += 6), '\0', FALSE);
    if (sec < 0)
      return FAIL;
    cd->retry = sec;
    }
  else
    return FAIL;
return OK;
}
#endif



/*************************************************
*          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:
  malware_re    match condition for "malware="
  cacheable	the RE did not use any dynamic elements during expansion
  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

Returns:        Exim message processing code (OK, FAIL, DEFER, ...)
                where true means malware was found (condition applies)
*/
static int
malware_internal(const uschar * malware_re, BOOL cacheable,
  const uschar * scan_filename, int timeout)
{
int sep = 0;
const uschar *av_scanner_work = av_scanner;
BOOL av_scanner_textonly;
uschar *scanner_name;
unsigned long mbox_size;
FILE *mbox_file;
const pcre2_code *re;
uschar * errstr;
struct scan * scanent;
const uschar * scanner_options;
client_conn_ctx malware_daemon_ctx = {.sock = -1};
time_t tmo;
const uschar * eml_filename, * eml_dir;

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_panic_defer(US"error while creating mbox spool 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. */

(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)
  return FAIL;		/* explicitly no matching */

/* special cases (match anything except empty) */
if (  strcmpic(malware_re, US"true") == 0
   || Ustrcmp(malware_re, "*") == 0
   || Ustrcmp(malware_re, "1") == 0
   )
  {
  if (  !malware_default_re
     && !(malware_default_re = m_pcre_compile(malware_regex_default, FALSE, &errstr)))
    return malware_panic_defer(errstr);
  malware_re = malware_regex_default;
  re = malware_default_re;
  }

/* compile the regex, see if it works */
else if (!(re = m_pcre_compile(malware_re, cacheable, &errstr)))
  return malware_panic_defer(errstr);

/* if av_scanner starts with a dollar, expand it first */
if (*av_scanner == '$')
  {
  if (!(av_scanner_work = expand_string_2(av_scanner, &av_scanner_textonly)))
    return malware_panic_defer(
	 string_sprintf("av_scanner starts with $, but expansion failed: %s",
	 expand_string_message));

  DEBUG(acl)
    debug_printf_indent("Expanded av_scanner global: %s\n", av_scanner_work);
  /* disable result caching in this case */
  malware_name = NULL;
  malware_ok = FALSE;
  }
else
  av_scanner_textonly = TRUE;

/* Do not scan twice (unless av_scanner is dynamic). */
if (!malware_ok)
  {
  /* find the scanner type from the av_scanner option */
  if (!(scanner_name = string_nextinlist(&av_scanner_work, &sep, NULL, 0)))
    return malware_panic_defer(US"av_scanner configuration variable is empty");
  if (!timeout) timeout = MALWARE_TIMEOUT;
  tmo = time(NULL) + timeout;

  for (scanent = m_scans; ; scanent++)
    {
    if (!scanent->name)
      return malware_panic_defer(string_sprintf("unknown scanner type '%s'",
	scanner_name));
    if (strcmpic(scanner_name, US scanent->name) != 0)
      continue;
    DEBUG(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(acl) debug_printf_indent("%15s%10s%s\n", "", "socket: ", scanner_options);
    switch(scanent->conn)
    {
    case MC_TCP:
      malware_daemon_ctx.sock = ip_tcpsocket(scanner_options, &errstr, 5, NULL); break;
    case MC_UNIX:
      malware_daemon_ctx.sock = ip_unixsocket(scanner_options, &errstr);	break;
    case MC_STRM:
      malware_daemon_ctx.sock = ip_streamsocket(scanner_options, &errstr, 5, NULL); break;
    default:
      /* compiler quietening */ break;
    }
    if (malware_daemon_ctx.sock < 0)
      return m_panic_defer(scanent, CUS callout_address, errstr);
    break;
  }

  switch (scanent->scancode)
    {
#ifndef DISABLE_MAL_CMDLINE
    case M_CMDL: /* "cmdline" scanner type ---------------------------------- */
      {
      const uschar *cmdline_scanner = scanner_options;
      const pcre2_code *cmdline_trigger_re;
      const pcre2_code *cmdline_regex_re;
      uschar * file_name;
      uschar * commandline;
      void (*eximsigchld)(int);
      void (*eximsigpipe)(int);
      FILE *scanner_out = NULL;
      int scanner_fd;
      FILE *scanner_record = NULL;
      uschar linebuffer[32767];
      int rcnt;
      int trigger = 0;
      uschar *p;

      if (!cmdline_scanner)
	return m_panic_defer(scanent, NULL, errstr);

      /* find scanner output trigger */
      cmdline_trigger_re = m_pcre_nextinlist(&av_scanner_work, &sep, av_scanner_textonly,
				"missing trigger specification", &errstr);
      if (!cmdline_trigger_re)
	return m_panic_defer(scanent, NULL, errstr);

      /* find scanner name regex */
      cmdline_regex_re = m_pcre_nextinlist(&av_scanner_work, &sep, av_scanner_textonly,
			  "missing virus name regex specification", &errstr);
      if (!cmdline_regex_re)
	return m_panic_defer(scanent, NULL, errstr);

      /* prepare scanner call; despite the naming, file_name holds a directory
      name which is documented as the value given to %s. */

      file_name = string_copy(eml_filename);
      p = Ustrrchr(file_name, '/');
      if (p)
	*p = '\0';
      commandline = string_sprintf(CS cmdline_scanner, file_name);

      /* redirect STDERR too */
      commandline = string_sprintf("%s 2>&1", commandline);

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

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

      if (!(scanner_out = popen(CS commandline,"r")))
	{
	int err = errno;
	signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe);
	return m_panic_defer(scanent, NULL,
	  string_sprintf("call (%s) failed: %s.", commandline, strerror(err)));
	}
      scanner_fd = fileno(scanner_out);

      file_name = string_sprintf("%s/%s_scanner_output", eml_dir, message_id);

      if (!(scanner_record = modefopen(file_name, "wb", SPOOL_MODE)))
	{
	int err = errno;
	(void) pclose(scanner_out);
	signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe);
	return m_panic_defer(scanent, NULL, string_sprintf(
	    "opening scanner output file (%s) failed: %s.",
	    file_name, strerror(err)));
	}

      /* look for trigger while recording output */
      while ((rcnt = recv_line(scanner_fd, linebuffer,
		      sizeof(linebuffer), tmo)))
	{
	if (rcnt < 0)
	  {
	  int err = errno;
	  if (rcnt == -1)
	    break;
	  (void) pclose(scanner_out);
	  signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe);
	  return m_panic_defer(scanent, NULL, string_sprintf(
	      "unable to read from scanner (%s): %s",
	      commandline, strerror(err)));
	  }

	if (Ustrlen(linebuffer) > fwrite(linebuffer, 1, Ustrlen(linebuffer), scanner_record))
	  {
	  /* short write */
	  (void) pclose(scanner_out);
	  signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe);
	  return m_panic_defer(scanent, NULL, string_sprintf(
	    "short write on scanner output file (%s).", file_name));
	  }
	putc('\n', scanner_record);
	/* try trigger match */
	if (  !trigger
	   && regex_match_and_setup(cmdline_trigger_re, linebuffer, 0, -1)
	   )
	  trigger = 1;
	}

      (void)fclose(scanner_record);
      sep = pclose(scanner_out);
      signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe);
      if (sep != 0)
	  return m_panic_defer(scanent, NULL,
	      sep == -1
	      ? string_sprintf("running scanner failed: %s", strerror(sep))
	      : string_sprintf("scanner returned error code: %d", sep));

      if (trigger)
	{
	uschar * s;
	/* setup default virus name */
	malware_name = US"unknown";

	/* re-open the scanner output file, look for name match */
	scanner_record = Ufopen(file_name, "rb");
	while (Ufgets(linebuffer, sizeof(linebuffer), scanner_record))
	  if ((s = m_pcre_exec(cmdline_regex_re, linebuffer))) /* try match */
	    malware_name = s;
	(void)fclose(scanner_record);
	}
      else /* no virus found */
	malware_name = NULL;
      break;
      }	/* cmdline */
#endif

#ifndef DISABLE_MAL_CLAM
    case M_CLAMD: /* "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
* See Exim bug 926 for details.  */

      uschar *p, *vname;
      int bread=0;
      uschar av_buffer[1024];
      uschar *hostname = US"";
      host_item connhost;
      int clam_fd;
      unsigned int fsize_uint;
      BOOL use_scan_command = FALSE;
      clamd_address * cv[MAX_CLAMD_SERVERS];
      int num_servers = 0;
      uint32_t send_size, send_final_zeroblock;
      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 */

      if (*scanner_options == '/')
	{
	clamd_address * cd;
	const uschar * sublist;
	int subsep = ' ';

	/* Local file; so we def want to use_scan_command and don't want to try
	passing IP/port combinations */
	use_scan_command = TRUE;
	cd = (clamd_address *) store_get(sizeof(clamd_address), GET_UNTAINTED);

	/* extract socket-path part */
	sublist = scanner_options;
	cd->hostspec = string_nextinlist(&sublist, &subsep, NULL, 0);

	/* parse options */
	if (clamd_option(cd, sublist, &subsep) != OK)
	  return m_panic_defer(scanent, NULL,
	    string_sprintf("bad option '%s'", scanner_options));
	cv[0] = cd;
	}
      else
	{
	/* Go through the rest of the list of host/port and construct an array
	 * of servers to try. The first one is the bit we just passed from
	 * scanner_options so process that first and then scan the remainder of
	 * the address buffer */
	do
	  {
	  clamd_address * cd;
	  const uschar * sublist;
	  int subsep = ' ';
	  uschar * s;

	  /* The 'local' option means use the SCAN command over the network
	   * socket (ie common file storage in use) */
	  /*XXX we could accept this also as a local option? */
	  if (strcmpic(scanner_options, US"local") == 0)
	    {
	    use_scan_command = TRUE;
	    continue;
	    }

	  cd = (clamd_address *) store_get(sizeof(clamd_address), GET_UNTAINTED);

	  /* extract host and port part */
	  sublist = scanner_options;
	  if (!(cd->hostspec = string_nextinlist(&sublist, &subsep, NULL, 0)))
	    {
	    (void) m_panic_defer(scanent, NULL,
		      string_sprintf("missing address: '%s'", scanner_options));
	    continue;
	    }
	  if (!(s = string_nextinlist(&sublist, &subsep, NULL, 0)))
	    {
	    (void) m_panic_defer(scanent, NULL,
		      string_sprintf("missing port: '%s'", scanner_options));
	    continue;
	    }
	  cd->tcp_port = atoi(CS s);

	  /* parse options */
	  /*XXX should these options be common over scanner types? */
	  if (clamd_option(cd, sublist, &subsep) != OK)
	    return m_panic_defer(scanent, NULL,
	      string_sprintf("bad option '%s'", scanner_options));

	  cv[num_servers++] = cd;
	  if (num_servers >= MAX_CLAMD_SERVERS)
	    {
	    (void) m_panic_defer(scanent, NULL,
		  US"More than " MAX_CLAMD_SERVERS_S " clamd servers "
		  "specified; only using the first " MAX_CLAMD_SERVERS_S );
	    break;
	    }
	  } while ((scanner_options = string_nextinlist(&av_scanner_work, &sep,
					NULL, 0)));

	/* check if we have at least one server */
	if (!num_servers)
	  return m_panic_defer(scanent, NULL,
	    US"no useable server addresses in malware configuration option.");
	}

      /* 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, ':'))
	return m_panic_defer(scanent, NULL,
	  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)
	{ cmd_str.data = US"zINSTREAM"; cmd_str.len = 10; }
      else
	{
	int n;
	cmd_str.data = string_sprintf("SCAN %s\n%n", eml_filename, &n);
	cmd_str.len = n;		/* .len is a size_t */
	}

      /* We have some network servers specified */
      if (num_servers)
	{
	/* 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. */

	while (num_servers > 0)
	  {
	  int i = random_number(num_servers);
	  clamd_address * cd = cv[i];

	  DEBUG(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 (;;)
	    {
	    /*XXX we trust that the cmd_str is idempotent */
	    if ((malware_daemon_ctx.sock = m_tcpsocket(cd->hostspec, cd->tcp_port,
				    &connhost, &errstr,
				    use_scan_command ? &cmd_str : NULL)) >= 0)
	      {
	      /* Connection successfully established with a server */
	      hostname = cd->hostspec;
	      if (use_scan_command) cmd_str.len = 0;
	      break;
	      }
	    if (cd->retry <= 0) break;
	    while (cd->retry > 0) cd->retry = sleep(cd->retry);
	    }
	  if (malware_daemon_ctx.sock >= 0)
	    break;

	  (void) m_panic_defer(scanent, CUS callout_address, errstr);

	  /* Remove the server from the list. XXX We should free the memory */
	  num_servers--;
	  for (; i < num_servers; i++)
	    cv[i] = cv[i+1];
	  }

	if (num_servers == 0)
	  return m_panic_defer(scanent, NULL, US"all servers failed");
	}
      else
	for (;;)
	  {
	  if ((malware_daemon_ctx.sock = ip_unixsocket(cv[0]->hostspec, &errstr)) >= 0)
	    {
	    hostname = cv[0]->hostspec;
	    break;
	    }
	  if (cv[0]->retry <= 0)
	    return m_panic_defer(scanent, CUS callout_address, errstr);
	  while (cv[0]->retry > 0) cv[0]->retry = sleep(cv[0]->retry);
	  }

      /* 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)
	{
	struct stat st;
#if defined(EXIM_TCP_CORK) && !defined(OS_SENDFILE)
	BOOL corked = TRUE;
#endif
	/* 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. We only send one chunk. */

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

	cork_fd(malware_daemon_ctx.sock);

	/* Pass the string to ClamAV (10 = "zINSTREAM\0"), if not already sent */
	if (cmd_str.len)
	  if (send(malware_daemon_ctx.sock, cmd_str.data, cmd_str.len, 0) < 0)
	    return m_panic_defer_3(scanent, CUS hostname,
	      string_sprintf("unable to send zINSTREAM to socket (%s)",
		strerror(errno)),
	      malware_daemon_ctx.sock);

	if ((clam_fd = exim_open2(CS eml_filename, O_RDONLY)) < 0)
	  {
	  int err = errno;
	  return m_panic_defer_3(scanent, NULL,
	    string_sprintf("can't open spool file %s: %s",
	      eml_filename, strerror(err)),
	    malware_daemon_ctx.sock);
	  }
	if (fstat(clam_fd, &st) < 0)
	  {
	  int err = errno;
	  (void)close(clam_fd);
	  return m_panic_defer_3(scanent, NULL,
	    string_sprintf("can't stat spool file %s: %s",
	      eml_filename, strerror(err)),
	    malware_daemon_ctx.sock);
	  }
	fsize_uint = (unsigned int) st.st_size;
	if ((off_t)fsize_uint != st.st_size)
	  {
	  (void)close(clam_fd);
	  return m_panic_defer_3(scanent, NULL,
	    string_sprintf("stat spool file %s, size overflow", eml_filename),
	    malware_daemon_ctx.sock);
	  }

	/* send file size */
	send_size = htonl(fsize_uint);
	if (send(malware_daemon_ctx.sock, &send_size, sizeof(send_size), 0) < 0)
	  return m_panic_defer_3(scanent, NULL,
	    string_sprintf("unable to send file size to socket (%s)", hostname),
	    malware_daemon_ctx.sock);

	/* send file body */
	while (fsize_uint)
	  {
#ifdef OS_SENDFILE
	  int n = os_sendfile(malware_daemon_ctx.sock, clam_fd, NULL, (size_t)fsize_uint);
	  if (n < 0)
	    return m_panic_defer_3(scanent, NULL,
	      string_sprintf("unable to send file body to socket (%s): %s", hostname, strerror(errno)),
	      malware_daemon_ctx.sock);
	  fsize_uint -= n;
#else
	  int n = MIN(fsize_uint, big_buffer_size);
	  if ((n = read(clam_fd, big_buffer, n)) < 0)
	    return m_panic_defer_3(scanent, NULL,
	      string_sprintf("can't read spool file %s: %s",
		eml_filename, strerror(errno)),
	      malware_daemon_ctx.sock);
	  if (send(malware_daemon_ctx.sock, big_buffer, (size_t)n, 0) < 0)
	    return m_panic_defer_3(scanent, NULL,
	      string_sprintf("unable to send file body to socket (%s): %s", hostname, strerror(errno)),
	      malware_daemon_ctx.sock);
	  fsize_uint -= n;
# ifdef EXIM_TCP_CORK
	  if (corked)
	    {
	    corked = FALSE;
	    uncork_fd(malware_daemon_ctx.sock);
	    }
# endif
#endif	/*!OS_SENDFILE*/

	  }

	send_final_zeroblock = 0;
	if (send(malware_daemon_ctx.sock, &send_final_zeroblock, sizeof(send_final_zeroblock), 0) < 0)
	  return m_panic_defer_3(scanent, NULL,
	    string_sprintf("unable to send file terminator to socket (%s)", hostname),
	    malware_daemon_ctx.sock);
#ifdef OS_SENDFILE
	uncork_fd(malware_daemon_ctx.sock);
#endif
	}
      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), if not already sent */

	DEBUG(acl) debug_printf_indent(
	    "Malware scan: issuing %s local-path scan [%s]\n",
	    scanner_name, scanner_options);

	if (cmd_str.len)
	  if (send(malware_daemon_ctx.sock, cmd_str.data, cmd_str.len, 0) < 0)
	    return m_panic_defer_3(scanent, CUS callout_address,
	      string_sprintf("unable to write to socket (%s)", strerror(errno)),
	      malware_daemon_ctx.sock);

	/* 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(&malware_daemon_ctx, av_buffer, sizeof(av_buffer), tmo);
      (void)close(malware_daemon_ctx.sock);
      malware_daemon_ctx.sock = -1;
      malware_daemon_ctx.tls_ctx = NULL;

      if (bread <= 0)
	return m_panic_defer(scanent, CUS callout_address,
	  string_sprintf("unable to read from socket (%s)",
	  errno == 0 ? "EOF" : strerror(errno)));

      if (bread == sizeof(av_buffer))
	return m_panic_defer(scanent, CUS callout_address,
		US"buffer too small");
      /* We're now assured of a NULL at the end of av_buffer */

      /* 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))
	return m_panic_defer(scanent, CUS callout_address,
		US"ClamAV returned null");

      /* 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(acl) debug_printf_indent("Malware response: %s\n", av_buffer);

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

      /* colon in returned output? */
      if (!(p = Ustrchr(av_buffer,':')))
	return m_panic_defer(scanent, CUS callout_address, string_sprintf(
		  "ClamAV returned malformed result (missing colon): %s",
		  av_buffer));

      /* do the OK check first and bail early if we can */
      if (Ustrcmp(++p, " OK") == 0)
	{
	/* Everything should be OK */
	malware_name = NULL;
	DEBUG(acl) debug_printf_indent("Malware not found\n");
        break;
	}

      /* strip filename */
      while (isspace(*p)) 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, ' ');

      /* treat lack of space as unparseable error */
      if ( !p )
	return m_panic_defer(scanent, CUS callout_address,
	  string_sprintf("unparseable response from ClamAV: {%s}", av_buffer));

      if (Ustrcmp(p, " FOUND") == 0)
	{
	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';
	  }
	malware_name = string_copy(vname);
	DEBUG(acl) debug_printf_indent("Malware found, name %q\n", malware_name);

	}
      else if (Ustrcmp(p, " ERROR") == 0)
	return m_panic_defer(scanent, CUS callout_address,
	  string_sprintf("ClamAV returned: %s", av_buffer));

      else
	return m_panic_defer(scanent, CUS callout_address,
	  string_sprintf("unparseable response from ClamAV: {%s}", av_buffer));

      break;
      } /* clamd */
#endif

#ifndef DISABLE_MAL_SOCK
    case M_SOCK: /* "sock" scanner type ------------------------------------- */
    /* This code was derived by Martin Poole from the clamd code contributed
       by David Saez and the cmdline code
    */
      {
      int bread;
      uschar * commandline;
      uschar av_buffer[1024];
      uschar * linebuffer;
      uschar * sockline_scanner;
      uschar sockline_scanner_default[] = "%s\n";
      const pcre2_code *sockline_trig_re;
      const pcre2_code *sockline_name_re;

      /* find scanner command line */
      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++)
	  if ((*s != 's' && *s != '%') || Ustrchr(s+1, '%'))
	    return m_panic_defer_3(scanent, NULL,
				  US"unsafe sock scanner call spec", malware_daemon_ctx.sock);
      }
      else
	sockline_scanner = sockline_scanner_default;
      DEBUG(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, av_scanner_textonly,
				"missing trigger specification", &errstr);
      if (!sockline_trig_re)
	return m_panic_defer_3(scanent, NULL, errstr, malware_daemon_ctx.sock);

      /* find virus name regex */
      sockline_name_re = m_pcre_nextinlist(&av_scanner_work, &sep, av_scanner_textonly,
			  "missing virus name regex specification", &errstr);
      if (!sockline_name_re)
	return m_panic_defer_3(scanent, NULL, errstr, malware_daemon_ctx.sock);

      /* prepare scanner call - security depends on expansions check above */
      commandline = string_sprintf( CS sockline_scanner, eml_filename);
      DEBUG(acl) debug_printf_indent("%15s%10s'%s'\n", "", "expanded: ",
	string_printing(commandline));

      /* Pass the command string to the socket */
      if (m_sock_send(malware_daemon_ctx.sock, commandline, Ustrlen(commandline), &errstr) < 0)
	return m_panic_defer(scanent, CUS callout_address, errstr);

      /* Read the result */
      bread = ip_recv(&malware_daemon_ctx, av_buffer, sizeof(av_buffer), tmo);

      if (bread <= 0)
	return m_panic_defer_3(scanent, CUS callout_address,
	  string_sprintf("unable to read from socket (%s)", strerror(errno)),
	  malware_daemon_ctx.sock);

      if (bread == sizeof(av_buffer))
	return m_panic_defer_3(scanent, CUS callout_address,
		US"buffer too small", malware_daemon_ctx.sock);
      av_buffer[bread] = '\0';
      linebuffer = string_copy(av_buffer);
      DEBUG(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(acl) debug_printf_indent("%15s%10s'%s'\n", "", "name: ",
	  string_printing(malware_name));
	}
      else /* no virus found */
	malware_name = NULL;
      break;
      }
#endif

#ifndef DISABLE_MAL_AVAST
    case M_AVAST: /* "avast" scanner type ----------------------------------- */
      {
      uschar buf[1024];
      uschar * scanrequest;
      enum {AVA_HELO, AVA_OPT, AVA_RSP, AVA_DONE} avast_stage;
      int nread;
      uschar * error_message = NULL;
      BOOL more_data = FALSE;
      BOOL strict = TRUE;

      /* According to Martin Tuma @avast the protocol uses "escaped
      whitespace", that is, every embedded whitespace is backslash
      escaped, as well as backslash is protected by backslash.
      The returned lines contain the name of the scanned file, a tab
      and the [ ] marker.
      [+] - not infected
      [L] - infected
      [E] - some error occurred
      Such marker follows the first non-escaped TAB.  For more information
      see avast-protocol(5)

      We observed two cases:
      -> SCAN /file
      <- /file [E]0.0 Error 13 Permission denied
      <- 451 SCAN Engine error 13 permission denied

      -> SCAN /file
      <- /file… [E]3.0 Error 41120 The file is a decompression bomb
      <- /file… [+]2.0
      <- /file… [+]2.0 0 Eicar Test Virus!!!
      <- 200 SCAN OK

      If the scanner returns 4xx, DEFER is a good decision, combined
      with a panic log entry, to get the admin's attention.

      If the scanner returns 200, we reject it as malware, if found any,
      or, in case of an error, we set the malware message to the error
      string.

      Some of the >= 42000 errors are message related - usually some
      broken archives etc, but some of them are e.g. license related.
      Once the license expires the engine starts returning errors for
      every scanning attempt.  I¹ have the full list of the error codes
      but it is not a public API and is subject to change. It is hard
      for me to say what you should do in case of an engine error. You
      can have a “Treat * unscanned file as infection” policy or “Treat
      unscanned file as clean” policy.  ¹) Jakub Bednar

       */

      if (  (  !ava_re_clean
            && !(ava_re_clean = m_pcre_compile(ava_re_clean_str, FALSE, &errstr)))
	 || (  !ava_re_virus
	    && !(ava_re_virus = m_pcre_compile(ava_re_virus_str, FALSE, &errstr)))
	 || (  !ava_re_error
	    && !(ava_re_error = m_pcre_compile(ava_re_error_str, FALSE, &errstr)))
	 )
	return malware_panic_defer(errstr);

      /* wait for result */
      for (avast_stage = AVA_HELO;
	   (nread = recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo)) > 0;
	  )
	{
	int slen = Ustrlen(buf);
	if (slen >= 1)
	  {

          /* Multi line responses are bracketed between 210 … and nnn … */
          if (Ustrncmp(buf, "210", 3) == 0)
            {
            more_data = 1;
            continue;
            }
          else if (more_data && isdigit(buf[0])) more_data = 0;

	  switch (avast_stage)
	    {
	    case AVA_HELO:
              if (more_data) continue;
	      if (Ustrncmp(buf, "220", 3) != 0)
		goto endloop;			/* require a 220 */
	      goto sendreq;

	    case AVA_OPT:
              if (more_data) continue;
	      if (Ustrncmp(buf, "200", 3) != 0)
		goto endloop;			/* require a 200 */

	    sendreq:
	      {
	      int len;
	      /* Check for another option to send. Newline-terminate it. */
	      if ((scanrequest = string_nextinlist(&av_scanner_work, &sep,
				NULL, 0)))
		{
                if (Ustrcmp(scanrequest, "pass_unscanned") == 0)
                  {
                  DEBUG(acl) debug_printf_indent("pass unscanned files as clean\n");
                  strict = FALSE;
                  goto sendreq;
                  }
		scanrequest = string_sprintf("%s\n", scanrequest);
		avast_stage = AVA_OPT;		/* just sent option */
		DEBUG(acl) debug_printf_indent("send to avast OPTION: %s", scanrequest);
		}
	      else
		{
		scanrequest = string_sprintf("SCAN %s\n", eml_dir);
		avast_stage = AVA_RSP;		/* just sent command */
		DEBUG(acl) debug_printf_indent("send to avast REQUEST: SCAN %s\n", eml_dir);
		}

	      /* send config-cmd or scan-request to socket */
	      len = Ustrlen(scanrequest);
	      if (send(malware_daemon_ctx.sock, scanrequest, len, 0) == -1)
		{
		scanrequest[len-1] = '\0';
		return m_panic_defer_3(scanent, CUS callout_address, string_sprintf(
		      "unable to send request '%s' to socket (%s): %s",
		      scanrequest, scanner_options, strerror(errno)), malware_daemon_ctx.sock);
		}
	      break;
	      }

	    case AVA_RSP:

	      if (isdigit(buf[0]))  /* We're done */
                goto endloop;

              if (malware_name)     /* Nothing else matters, just read on */
                break;

	      if (regex_match(ava_re_clean, buf, slen, NULL))
		break;

              if ((malware_name = m_pcre_exec(ava_re_virus, buf)))
                {
                unescape(malware_name);
                DEBUG(acl)
                  debug_printf_indent("unescaped malware name: '%s'\n", malware_name);
                break;
                }

              if (strict)           /* treat scanner errors as malware */
                {
                if ((malware_name = m_pcre_exec(ava_re_error, buf)))
                  {
                  unescape(malware_name);
                  DEBUG(acl)
                    debug_printf_indent("unescaped error message: '%s'\n", malware_name);
                  break;
                  }
                }
              else if (regex_match(ava_re_error, buf, slen, NULL))
                {
                log_write(LOG_MAIN, "internal scanner error (ignored): %s", buf);
                break;
                }

	      /* here also for any unexpected response from the scanner */
              DEBUG(acl) debug_printf("avast response not handled: '%s'\n", buf);

	      goto endloop;

	    default:	log_write(LOG_PANIC, "%s:%d:%s: should not happen",
			    __FILE__, __LINE__, __FUNCTION__);
	    }
	  }
	}

      endloop:

      if (nread == -1) error_message = US"EOF from scanner";
      else if (nread < 0) error_message = US"timeout from scanner";
      else if (nread == 0) error_message = US"got nothing from scanner";
      else if (buf[0] != '2') error_message = buf;

      DEBUG(acl) debug_printf_indent("sent to avast QUIT\n");
      if (send(malware_daemon_ctx.sock, "QUIT\n", 5, 0) == -1)
        return m_panic_defer_3(scanent, CUS callout_address,
          string_sprintf("unable to send quit request to socket (%s): %s",
            scanner_options, strerror(errno)), malware_daemon_ctx.sock);

      if (error_message)
        return m_panic_defer_3(scanent, CUS callout_address, error_message, malware_daemon_ctx.sock);

      }
#endif
  default:	break;	/* compiler quietening */
  }	/* scanner type switch */

  if (malware_daemon_ctx.sock >= 0)
    (void) close (malware_daemon_ctx.sock);
  malware_ok = TRUE;			/* set "been here, done that" marker */
  }

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


/*************************************************
*          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:
  malware_re  match condition for "malware="
  cacheable   the RE did not use any dynamic elements during expansion
  timeout     if nonzero, timeout in seconds

Returns:      Exim message processing code (OK, FAIL, DEFER, ...)
              where true means malware was found (condition applies)
*/
int
malware(const uschar * malware_re, BOOL cacheable, int timeout)
{
int ret = malware_internal(malware_re, cacheable, NULL, timeout);

if (ret == DEFER) av_failed = TRUE;
return ret;
}


/*************************************************
*          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(const 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, rf_notify_unset,NULL);
f.enable_dollar_recipients = TRUE;

ret = malware_internal(US"*", TRUE, eml_filename, 0);

memcpy(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;
}


void
malware_init(void)
{
if (!malware_default_re)
  malware_default_re = regex_must_compile(malware_regex_default, MCS_NOFLAGS, TRUE);

#ifndef DISABLE_MAL_AVAST
if (!ava_re_clean)
  ava_re_clean = regex_must_compile(ava_re_clean_str, MCS_NOFLAGS, TRUE);
if (!ava_re_virus)
  ava_re_virus = regex_must_compile(ava_re_virus_str, MCS_NOFLAGS, TRUE);
if (!ava_re_error)
  ava_re_error = regex_must_compile(ava_re_error_str, MCS_NOFLAGS, TRUE);
#endif
}


gstring *
malware_show_supported(gstring * g)
{
g = string_cat(g, US"Malware:");
for (struct scan * sc = m_scans; sc->scancode != (scanner_t)-1; sc++)
  g = string_fmt_append(g, " %s", sc->name);
return string_cat(g, US"\n");
}


# endif	/*!MACRO_PREDEF*/
#endif /*WITH_CONTENT_SCAN*/
/*
 * vi: aw ai sw=2
 */
