[exim-cvs] Add retry option to clamd and spamd. Bug 392

Top Page
Delete this message
Reply to this message
Author: Exim Git Commits Mailing List
Date:  
To: exim-cvs
Subject: [exim-cvs] Add retry option to clamd and spamd. Bug 392
Gitweb: http://git.exim.org/exim.git/commitdiff/8a512ed5b7f75c8aaedbca887257ee01e5c2b621
Commit:     8a512ed5b7f75c8aaedbca887257ee01e5c2b621
Parent:     4c01d6abf6429fff8ca8a97027dc9ac965f477a3
Author:     Jeremy Harris <jgh146exb@???>
AuthorDate: Mon Feb 2 00:11:05 2015 +0000
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Mon Feb 9 16:00:55 2015 +0000


    Add retry option to clamd and spamd.  Bug 392
---
 doc/doc-docbook/spec.xfpt       |   48 +++++++--
 doc/doc-txt/ChangeLog           |    3 +
 src/src/ip.c                    |    5 +-
 src/src/malware.c               |  225 +++++++++++++++++++++++++--------------
 src/src/spam.c                  |   82 ++++++++------
 src/src/spam.h                  |    2 +
 test/README                     |    4 +-
 test/confs/4005                 |    6 +-
 test/confs/4009                 |    4 +-
 test/log/4005                   |    3 +
 test/log/4009                   |    8 ++
 test/scripts/4000-scanning/4005 |   21 ++++-
 test/scripts/4000-scanning/4009 |  123 +++++++++++++++++++++
 test/src/server.c               |   15 +++-
 test/stdout/4005                |   18 +++
 test/stdout/4009                |  111 +++++++++++++++++++
 16 files changed, 545 insertions(+), 133 deletions(-)


diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index b5133a2..dfe0432 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -30470,17 +30470,39 @@ av_scanner = aveserver:/var/run/aveserver
 This daemon-type scanner is GPL and free. You can get it at
 &url(http://www.clamav.net/). Some older versions of clamd do not seem to
 unpack MIME containers, so it used to be recommended to unpack MIME attachments
-in the MIME ACL. This no longer believed to be necessary. One option is
-required: either the path and name of a UNIX socket file, or a hostname or IP
-number, and a port, separated by space, as in the second of these examples:
+in the MIME ACL. This is no longer believed to be necessary.
+
+The options are a list of server specifiers, which may be
+a UNIX socket specification,
+a TCP socket specification,
+or a (global) option.
+
+A socket specification consists of a space-separated list.
+For a Unix socket the first element is a full path for the socket,
+for a TCP socket the first element is the IP address
+and the second a port number,
+Any further elements are per-server (non-global) options.
+These per-server options are supported:
+.code
+retry=<timespec>    Retry on connect fail
+.endd
+
+The &`retry`& option specifies a time after which a single retry for
+a failed connect is made.  The default is to not retry.
+
+If a Unix socket file is specified, only one server is supported.
+
+Examples:
 .code
 av_scanner = clamd:/opt/clamd/socket
 av_scanner = clamd:192.0.2.3 1234
 av_scanner = clamd:192.0.2.3 1234:local
+av_scanner = clamd:192.0.2.3 1234 retry=10s
 av_scanner = clamd:192.0.2.3 1234 : 192.0.2.4 1234
 .endd
-If the value of av_scanner points to a UNIX socket file or contains the local
-keyword, then the ClamAV interface will pass a filename containing the data
+If the value of av_scanner points to a UNIX socket file or contains the
+&`local`&
+option, then the ClamAV interface will pass a filename containing the data
 to be scanned, which will should normally result in less I/O happening and be
 more efficient.  Normally in the TCP case, the data is streamed to ClamAV as
 Exim does not assume that there is a common filesystem with the remote host.
@@ -30731,8 +30753,7 @@ deny message = This message contains malware ($malware_name)
 The &%spam%& ACL condition calls SpamAssassin's &%spamd%& daemon to get a spam
 score and a report for the message.
 .new
-Support is also provided for Rspamd (which can speak SpamAssassin's protocol but
-provides reduced functionality when used in this mode).
+Support is also provided for Rspamd.


 For more information about installation and configuration of SpamAssassin or 
 Rspamd refer to their respective websites at
@@ -30802,9 +30823,10 @@ The supported option are:
 .code
 variant=rspamd      Use Rspamd rather than SpamAssassin protocol
 time=<start>-<end>  Use only between these times of day
-tmo=<timespec>      Connection time limit.
+tmo=<timespec>      Connection time limit
 weight=<value>      Selection bias
 backup              Use only if all non-backup servers fail
+retry=<timespec>    Retry on connect fail
 .endd


Time specifications for the &`time`& option are <hour>.<minute>.<second>
@@ -30812,10 +30834,16 @@ in the local time zone; each element being one or more digits.
Either the seconds or both minutes and seconds, plus the leading &`.`&
characters, may be omitted and will be taken as zero.

-Timeout specifications for the &`tmo`& option are the usual Exim
-time interval standard, eg. &`20s`& or &`1m`&.
+Timeout specifications for the &`tmo`& and &`retry`& options
+are the usual Exim time interval standard, eg. &`20s`& or &`1m`&.
+
+The &`tmo`& option specifies an overall timeout for communication.
The default value is two minutes.

+The &`retry`& option specifies a time after which a single retry for
+a failed connect is made.
+The default is to not retry.
+
 Servers are queried in a random fashion, weighted by the selection bias.
 The default value for selection bias is 1.
 .wen
diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog
index 7c5c7c8..c2959d3 100644
--- a/doc/doc-txt/ChangeLog
+++ b/doc/doc-txt/ChangeLog
@@ -67,6 +67,9 @@ JH/17 Bug 68: The spamd_address main option now supports an optional
 JH/18 Bug 1581: Router and transport options headers_add/remove can
       now have the list separator specified.


+JH/19 Bug 392: spamd_address, and clamd av_scanner, now support retry
+      option values. 
+



 Exim version 4.85
diff --git a/src/src/ip.c b/src/src/ip.c
index e4a43e6..f6072c2 100644
--- a/src/src/ip.c
+++ b/src/src/ip.c
@@ -337,7 +337,8 @@ for (h = &shost; h != NULL; h = h->next)
       {
       if (fd != fd6) close(fd6);
       if (fd != fd4) close(fd4);
-      if (connhost) {
+      if (connhost)
+    {
     h->port = port;
     *connhost = *h;
     connhost->next = NULL;
@@ -720,3 +721,5 @@ for (i=0; i < dscp_table_size; ++i)



/* End of ip.c */
+/* vi: aw ai sw=2
+*/
diff --git a/src/src/malware.c b/src/src/malware.c
index 2e49751..c13e706 100644
--- a/src/src/malware.c
+++ b/src/src/malware.c
@@ -38,14 +38,12 @@ static struct scan
/* The maximum number of clamd servers that are supported in the configuration */
#define MAX_CLAMD_SERVERS 32
#define MAX_CLAMD_SERVERS_S "32"
-/* Maximum length of the hostname that can be specified in the clamd address list */
-#define MAX_CLAMD_ADDRESS_LENGTH 64
-#define MAX_CLAMD_ADDRESS_LENGTH_S "64"

-typedef struct clamd_address_container {
- uschar tcp_addr[MAX_CLAMD_ADDRESS_LENGTH+1];
- unsigned int tcp_port;
-} clamd_address_container;
+typedef struct clamd_address {
+ uschar * hostspec;
+ unsigned tcp_port;
+ unsigned retry;
+} clamd_address;

#ifndef nelements
# define nelements(arr) (sizeof(arr) / sizeof(arr[0]))
@@ -115,21 +113,21 @@ extern uschar spooled_message_id[17];
static inline int
malware_errlog_defer(const uschar * str)
{
- log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: %s", str);
- return DEFER;
+log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: %s", str);
+return DEFER;
}

 static int
 m_errlog_defer(struct scan * scanent, const uschar * str)
 {
-  return malware_errlog_defer(string_sprintf("%s: %s", scanent->name, str));
+return malware_errlog_defer(string_sprintf("%s: %s", scanent->name, str));
 }
 static int
 m_errlog_defer_3(struct scan * scanent, const uschar * str,
     int fd_to_close)
 {
-  (void) close(fd_to_close);
-  return m_errlog_defer(scanent, str);
+(void) close(fd_to_close);
+return m_errlog_defer(scanent, str);
 }


 /*************************************************/
@@ -141,60 +139,61 @@ static inline int
 m_tcpsocket(const uschar * hostname, unsigned int port,
     host_item * host, uschar ** errstr)
 {
-  return ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5, host, errstr);
+return ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5, host, errstr);
 }


 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;
+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 pcre *
 m_pcre_compile(const uschar * re, uschar ** errstr)
 {
-  const uschar * rerror;
-  int roffset;
-  const pcre * cre;
-
-  cre = pcre_compile(CS re, PCRE_COPT, (const char **)&rerror, &roffset, NULL);
-  if (!cre)
-    *errstr= string_sprintf("regular expression error in '%s': %s at offset %d",
-    re, rerror, roffset);
-  return cre;
+const uschar * rerror;
+int roffset;
+const pcre * cre;
+
+cre = pcre_compile(CS re, PCRE_COPT, (const char **)&rerror, &roffset, NULL);
+if (!cre)
+  *errstr= string_sprintf("regular expression error in '%s': %s at offset %d",
+      re, rerror, roffset);
+return cre;
 }


 uschar *
 m_pcre_exec(const pcre * cre, uschar * text)
 {
-  int ovector[10*3];
-  int i = pcre_exec(cre, NULL, CS text, Ustrlen(text), 0, 0,
-        ovector, nelements(ovector));
-  uschar * substr = NULL;
-  if (i >= 2)                /* Got it */
-    pcre_get_substring(CS text, ovector, i, 1, (const char **) &substr);
-  return substr;
+int ovector[10*3];
+int i = pcre_exec(cre, NULL, CS text, Ustrlen(text), 0, 0,
+          ovector, nelements(ovector));
+uschar * substr = NULL;
+if (i >= 2)                /* Got it */
+  pcre_get_substring(CS text, ovector, i, 1, (const char **) &substr);
+return substr;
 }


static const pcre *
m_pcre_nextinlist(const uschar ** list, int * sep,
char * listerr, uschar ** errstr)
{
- const uschar * list_ele;
- const pcre * cre = NULL;
+const uschar * list_ele;
+const pcre * cre = NULL;

-  if (!(list_ele = string_nextinlist(list, sep, NULL, 0)))
-    *errstr = US listerr;
-  else
-    cre = m_pcre_compile(CUS list_ele, errstr);
-  return cre;
+if (!(list_ele = string_nextinlist(list, sep, NULL, 0)))
+  *errstr = US listerr;
+else
+  cre = m_pcre_compile(CUS list_ele, errstr);
+return cre;
 }


/*
@@ -375,6 +374,27 @@ if (mksd_read_lines (sock, av_buffer, sizeof (av_buffer), tmo) < 0)
return mksd_parse_line (scanent, CS av_buffer);
}

+
+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;
+  }
+}
+
 /*************************************************
 *          Scan content for malware              *
 *************************************************/
@@ -1205,7 +1225,7 @@ if (!malware_ok)
       off_t fsize;
       unsigned int fsize_uint;
       BOOL use_scan_command = FALSE;
-      clamd_address_container * clamd_address_vector[MAX_CLAMD_SERVERS];
+      clamd_address * cv[MAX_CLAMD_SERVERS];
       int num_servers = 0;
 #ifdef WITH_OLD_CLAMAV_STREAM
       unsigned int port;
@@ -1215,46 +1235,80 @@ if (!malware_ok)
       uint32_t send_size, send_final_zeroblock;
 #endif


+      /*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));
+
+    /* 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_errlog_defer(scanent,
+        string_sprintf("bad option '%s'", scanner_options));
+    cv[0] = cd;
+    }
       else
     {
-    const uschar *address = scanner_options;
-    uschar address_buffer[MAX_CLAMD_ADDRESS_LENGTH + 20];
-
     /* 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_container *this_clamd;
+      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) */
-      if (strcmpic(address,US"local") == 0)
+      /*XXX we could accept this also as a local option? */
+      if (strcmpic(scanner_options, US"local") == 0)
         {
         use_scan_command = TRUE;
         continue;
         }


-      /* XXX: If unsuccessful we should free this memory */
-      this_clamd =
-          (clamd_address_container *)store_get(sizeof(clamd_address_container));
+      cd = (clamd_address *) store_get(sizeof(clamd_address));


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


-      clamd_address_vector[num_servers] = this_clamd;
-      num_servers++;
+      /* parse options */
+      /*XXX should these options be common over scanner types? */
+      if (clamd_option(cd, sublist, &subsep) != OK)
+        {
+        return m_errlog_defer(scanent,
+          string_sprintf("bad option '%s'", scanner_options));
+        continue;
+        }
+
+      cv[num_servers++] = cd;
       if (num_servers >= MAX_CLAMD_SERVERS)
         {
         (void) m_errlog_defer(scanent,
@@ -1262,9 +1316,8 @@ if (!malware_ok)
           "specified; only using the first " MAX_CLAMD_SERVERS_S );
         break;
         }
-      } while ((address = string_nextinlist(&av_scanner_work, &sep,
-                    address_buffer,
-                    sizeof(address_buffer))) != NULL);
+      } while ((scanner_options = string_nextinlist(&av_scanner_work, &sep,
+                    NULL, 0)));


     /* check if we have at least one server */
     if (!num_servers)
@@ -1288,43 +1341,53 @@ if (!malware_ok)


     while (num_servers > 0)
       {
-      int i;
-      int current_server = 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",
-               clamd_address_vector[current_server]->tcp_addr,
-               clamd_address_vector[current_server]->tcp_port);
+      DEBUG(D_acl) debug_printf("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) */
-      sock= m_tcpsocket(clamd_address_vector[current_server]->tcp_addr,
-                  clamd_address_vector[current_server]->tcp_port,
-                  &connhost, &errstr);
-      if (sock >= 0)
+      for (;;)
         {
-        /* Connection successfully established with a server */
-        hostname = clamd_address_vector[current_server]->tcp_addr;
-        break;
+        sock= m_tcpsocket(cd->hostspec, cd->tcp_port, &connhost, &errstr);
+        if (sock >= 0)
+          {
+          /* Connection successfully established with a server */
+          hostname = cd->hostspec;
+          break;
+          }
+        if (cd->retry <= 0) break;
+        while (cd->retry > 0) cd->retry = sleep(cd->retry);
         }
+      if (sock >= 0)
+        break;


       log_write(0, LOG_MAIN, "malware acl condition: %s: %s",
         scanent->name, errstr);


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


     if (num_servers == 0)
       return m_errlog_defer(scanent, US"all servers failed");
     }
       else
-    {
-    if ((sock = ip_unixsocket(scanner_options, &errstr)) < 0)
-      return m_errlog_defer(scanent, errstr);
-    }
+    for (;;)
+      {
+      if ((sock = ip_unixsocket(cv[0]->hostspec, &errstr)) >= 0)
+        {
+        hostname = cv[0]->hostspec;
+        break;
+        }
+      if (cv[0]->retry <= 0)
+        return m_errlog_defer(scanent, 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
diff --git a/src/src/spam.c b/src/src/spam.c
index 99e524d..96780f5 100644
--- a/src/src/spam.c
+++ b/src/src/spam.c
@@ -21,7 +21,6 @@ int spam_ok = 0;
 int spam_rc = 0;
 uschar *prev_spamd_address_work = NULL;


-static int timeout_sec;
static const uschar * loglabel = US"spam acl condition:";


@@ -29,9 +28,11 @@ static int
spamd_param_init(spamd_address_container *spamd)
{
/* default spamd server weight, time and backup value */
-spamd->weight = SPAMD_WEIGHT;
spamd->is_failed = FALSE;
spamd->is_backup = FALSE;
+spamd->weight = SPAMD_WEIGHT;
+spamd->timeout = SPAMD_TIMEOUT;
+spamd->retry = 0;
return 0;
}

@@ -41,6 +42,7 @@ spamd_param(const uschar *param, spamd_address_container *spamd)
{
static int timesinceday = -1;
const uschar * s;
+const uschar * name;

/* check backup parameter */
if (Ustrcmp(param, "backup") == 0)
@@ -67,6 +69,7 @@ if (Ustrncmp(param, "time=", 5) == 0)
unsigned int time_start, time_end;
const uschar * end_string;

+  name = US"time";
   s = param+5;
   if ((end_string = Ustrchr(s, '-')))
     {
@@ -74,18 +77,10 @@ if (Ustrncmp(param, "time=", 5) == 0)
     if (  sscanf(CS end_string, "%u.%u.%u", &end_h,   &end_m,   &end_s)   == 0
        || sscanf(CS s,          "%u.%u.%u", &start_h, &start_m, &start_s) == 0
        )
-      {
-      log_write(0, LOG_MAIN,
-    "%s warning - invalid spamd time value: '%s'", loglabel, s);
-      return -1; /* syntax error */
-      }
+      goto badval;
     }
   else
-    {
-    log_write(0, LOG_MAIN,
-      "%s warning - invalid spamd time value: '%s'", loglabel, s);
-    return -1; /* syntax error */
-    }
+    goto badval;


   if (timesinceday < 0)
     {
@@ -112,19 +107,31 @@ if (Ustrcmp(param, "variant=rspamd") == 0)
 if (Ustrncmp(param, "tmo=", 4) == 0)
   {
   int sec = readconf_readtime((s = param+4), '\0', FALSE);
+  name = US"timeout";
   if (sec < 0)
-    {
-    log_write(0, LOG_MAIN,
-      "%s warning - invalid spamd timeout value: '%s'", loglabel, s);
-    return -1; /* syntax error */
-    }
-  timeout_sec = sec;
+    goto badval;
+  spamd->timeout = sec;
+  return 0;
+  }
+
+if (Ustrncmp(param, "retry=", 6) == 0)
+  {
+  int sec = readconf_readtime((s = param+6), '\0', FALSE);
+  name = US"retry";
+  if (sec < 0)
+    goto badval;
+  spamd->retry = sec;
   return 0;
   }


 log_write(0, LOG_MAIN, "%s warning - invalid spamd parameter: '%s'",
   loglabel, param);
 return -1; /* syntax error */
+
+badval:
+  log_write(0, LOG_MAIN,
+    "%s warning - invalid spamd %s value: '%s'", loglabel, name, s);
+  return -1; /* syntax error */
 }



@@ -188,7 +195,6 @@ FILE *mbox_file;
 int spamd_sock = -1;
 uschar spamd_buffer[32600];
 int i, j, offset, result;
-BOOL is_rspamd;
 uschar spamd_version[8];
 uschar spamd_short_result[8];
 uschar spamd_score_char;
@@ -206,6 +212,7 @@ struct timeval select_tv;         /* and applied by PH */
 fd_set select_fd;
 #endif
 uschar *spamd_address_work;
+spamd_address_container * sd;


 /* stop compiler warning */
 result = 0;
@@ -224,8 +231,6 @@ if ( (Ustrcmp(user_name,"0") == 0) ||
      (strcmpic(user_name,US"false") == 0) )
   return FAIL;


-timeout_sec = SPAMD_TIMEOUT;
-
/* if there is an additional option, check if it is "true" */
if (strcmpic(list,US"true") == 0)
/* in that case, always return true later */
@@ -278,7 +283,6 @@ start = time(NULL);
uschar *address;
const uschar *spamd_address_list_ptr = spamd_address_work;
spamd_address_container * spamd_address_vector[32];
- spamd_address_container * sd;

   /* Check how many spamd servers we have
      and register their addresses */
@@ -298,7 +302,7 @@ start = time(NULL);
      args++
      )
       {
-    HDEBUG(D_acl) debug_printf("spamd: addr parm '%s'\n", s);
+    HDEBUG(D_acl) debug_printf("spamd:  addr parm '%s'\n", s);
     switch (args)
     {
     case 0:   sd->hostspec = s;
@@ -331,17 +335,24 @@ start = time(NULL);
     goto defer;
     }


-  while (1)
+  current_server = spamd_get_server(spamd_address_vector, num_servers);
+  sd = spamd_address_vector[current_server];
+  for(;;)
     {
     uschar * errstr;


-    current_server = spamd_get_server(spamd_address_vector, num_servers);
-    sd = spamd_address_vector[current_server];
-
     debug_printf("trying server %s\n", sd->hostspec);


-    /* contact a spamd */
-    if ((spamd_sock = ip_streamsocket(sd->hostspec, &errstr, 5)) >= 0)
+    for (;;)
+      {
+      if (  (spamd_sock = ip_streamsocket(sd->hostspec, &errstr, 5)) >= 0
+         || sd->retry <= 0
+     )
+    break;
+      debug_printf("server %s: retry conn\n", sd->hostspec);
+      while (sd->retry > 0) sd->retry = sleep(sd->retry);
+      }
+    if (spamd_sock >= 0)
       break;


     log_write(0, LOG_MAIN, "%s spamd: %s", loglabel, errstr);
@@ -350,12 +361,11 @@ start = time(NULL);
     current_server = spamd_get_server(spamd_address_vector, num_servers);
     if (current_server < 0)
       {
-      log_write(0, LOG_MAIN|LOG_PANIC, "%s all spamd servers failed",
-    loglabel);
+      log_write(0, LOG_MAIN|LOG_PANIC, "%s all spamd servers failed", loglabel);
       goto defer;
       }
+    sd = spamd_address_vector[current_server];
     }
-    is_rspamd = sd->is_rspamd;
   }


if (spamd_sock == -1)
@@ -367,7 +377,7 @@ if (spamd_sock == -1)

 (void)fcntl(spamd_sock, F_SETFL, O_NONBLOCK);
 /* now we are connected to spamd on spamd_sock */
-if (is_rspamd)
+if (sd->is_rspamd)
   {                /* rspamd variant */
   uschar *req_str;
   const char *helo;
@@ -449,7 +459,7 @@ again:
       "%s %s on spamd socket", loglabel, strerror(errno));
       else
     {
-    if (time(NULL) - start < timeout_sec)
+    if (time(NULL) - start < sd->timeout)
       goto again;
     log_write(0, LOG_MAIN|LOG_PANIC,
       "%s timed out writing spamd socket", loglabel);
@@ -494,7 +504,7 @@ offset = 0;
 while ((i = ip_recv(spamd_sock,
            spamd_buffer + offset,
            sizeof(spamd_buffer) - offset - 1,
-           timeout_sec - time(NULL) + start)) > 0 )
+           sd->timeout - time(NULL) + start)) > 0 )
   offset += i;


/* error handling */
@@ -509,7 +519,7 @@ if (i <= 0 && errno != 0)
/* reading done */
(void)close(spamd_sock);

-if (is_rspamd)
+if (sd->is_rspamd)
   {                /* rspamd variant of reply */
   int r;
   if ((r = sscanf(CS spamd_buffer,
diff --git a/src/src/spam.h b/src/src/spam.h
index 7ab6d2a..0e3acfc 100644
--- a/src/src/spam.h
+++ b/src/src/spam.h
@@ -30,6 +30,8 @@ typedef struct spamd_address_container
   int is_failed:1;
   int is_backup:1;
   unsigned int weight;
+  unsigned int timeout;
+  unsigned int retry;
 } spamd_address_container;


#endif
diff --git a/test/README b/test/README
index 80c3511..e544857 100644
--- a/test/README
+++ b/test/README
@@ -897,13 +897,15 @@ input, details of which are given below. A number of options are implemented:

   -d       causes the server to output debugging information


-  -t       sets a timeout in seconds (default 5) for when the server is
+  -t <sec> sets a timeout (default 5) for when the server is
            awaiting an incoming connection


-noipv4 causes the server not to set up an IPv4 socket

-noipv6 causes the server not to set up an IPv6 socket

+ -i <sec> sets an initial pause, to delay before creating the listen sockets
+
By default, in an IPv6 environment, both kinds of socket are set up. However,
the test script knows which interfaces actually exist on the host, and it adds
-noipv4 or -noipv6 to the server command as required. An error occurs if both
diff --git a/test/confs/4005 b/test/confs/4005
index 8ed28d4..fd15dfc 100644
--- a/test/confs/4005
+++ b/test/confs/4005
@@ -1,6 +1,9 @@
# Exim test configuration 4005
# Content-scan: clamav interface

+OPT=
+CONTROL=
+
exim_path = EXIM_PATH
host_lookup_order = bydns
primary_hostname = myhost.test.ex
@@ -10,7 +13,8 @@ gecos_pattern = ""
gecos_name = CALLER_NAME
log_selector = +subject

-av_scanner = clamd : DIR/eximdir/clam_sock
+#XXX we need an additional test for tcp-connected clamd
+av_scanner = clamd : DIR/eximdir/clam_sock CONTROL

# ----- Main settings -----

diff --git a/test/confs/4009 b/test/confs/4009
index b635195..573aa6a 100644
--- a/test/confs/4009
+++ b/test/confs/4009
@@ -1,6 +1,8 @@
# Exim test configuration 4009
# Content-scan: spamassassin interface

+OPT=
+
exim_path = EXIM_PATH
host_lookup_order = bydns
primary_hostname = myhost.test.ex
@@ -10,7 +12,7 @@ gecos_pattern = ""
gecos_name = CALLER_NAME
log_selector = +subject

-spamd_address = 127.0.0.1 7833
+spamd_address = 127.0.0.1 7833 OPT

# ----- Main settings -----

diff --git a/test/log/4005 b/test/log/4005
index 7a4bb1c..821bed1 100644
--- a/test/log/4005
+++ b/test/log/4005
@@ -11,3 +11,6 @@
 1999-03-02 09:44:33 10HmaZ-0005vi-00 <= CALLER@??? U=CALLER P=local-esmtp S=sss T="accept this one despite timeout"
 1999-03-02 09:44:33 10HmaZ-0005vi-00 => :blackhole: <userx@???> R=r
 1999-03-02 09:44:33 10HmaZ-0005vi-00 Completed
+1999-03-02 09:44:33 10HmbC-0005vi-00 <= CALLER@??? U=CALLER P=local-esmtp S=sss T="message should be accepted after a retry"
+1999-03-02 09:44:33 10HmbC-0005vi-00 => :blackhole: <userx@???> R=r
+1999-03-02 09:44:33 10HmbC-0005vi-00 Completed
diff --git a/test/log/4009 b/test/log/4009
index 0aa7ba3..4522ddb 100644
--- a/test/log/4009
+++ b/test/log/4009
@@ -2,3 +2,11 @@
 1999-03-02 09:44:33 10HmaX-0005vi-00 <= CALLER@??? U=CALLER P=local-esmtp S=sss
 1999-03-02 09:44:33 10HmaX-0005vi-00 => :blackhole: <userx@???> R=r
 1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
+1999-03-02 09:44:33 10HmaY-0005vi-00 U=CALLER Warning: no action Spam detection software, running on the system "demo",\n has NOT identified this incoming email as spam.  The original\n message has been attached to this so you can view it or label\n similar future email.  If you have any questions, see\n @@CONTACT_ADDRESS@@ for details.\n \n Content preview:  test [...]\n \n Content analysis details:   (4.5 points, 5.0 required)\n \n  pts rule name              description\n ---- ---------------------- --------------------------------------------------\n -1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP\n  1.2 MISSING_HEADERS        Missing To: header\n  1.0 MISSING_FROM           Missing From: header\n  1.8 MISSING_SUBJECT        Missing Subject: header\n  1.4 MISSING_DATE           Missing Date: header\n  0.1 MISSING_MID            Missing Message-Id: header
+1999-03-02 09:44:33 10HmaY-0005vi-00 <= CALLER@??? U=CALLER P=local-esmtp S=sss
+1999-03-02 09:44:33 10HmaY-0005vi-00 => :blackhole: <userx@???> R=r
+1999-03-02 09:44:33 10HmaY-0005vi-00 Completed
+1999-03-02 09:44:33 10HmaZ-0005vi-00 U=CALLER Warning: no action Spam detection software, running on the system "demo",\n has NOT identified this incoming email as spam.  The original\n message has been attached to this so you can view it or label\n similar future email.  If you have any questions, see\n @@CONTACT_ADDRESS@@ for details.\n \n Content preview:  test [...]\n \n Content analysis details:   (4.5 points, 5.0 required)\n \n  pts rule name              description\n ---- ---------------------- --------------------------------------------------\n -1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP\n  1.2 MISSING_HEADERS        Missing To: header\n  1.0 MISSING_FROM           Missing From: header\n  1.8 MISSING_SUBJECT        Missing Subject: header\n  1.4 MISSING_DATE           Missing Date: header\n  0.1 MISSING_MID            Missing Message-Id: header
+1999-03-02 09:44:33 10HmaZ-0005vi-00 <= CALLER@??? U=CALLER P=local-esmtp S=sss
+1999-03-02 09:44:33 10HmaZ-0005vi-00 => :blackhole: <userx@???> R=r
+1999-03-02 09:44:33 10HmaZ-0005vi-00 Completed
diff --git a/test/scripts/4000-scanning/4005 b/test/scripts/4000-scanning/4005
index 0095157..d251c1a 100644
--- a/test/scripts/4000-scanning/4005
+++ b/test/scripts/4000-scanning/4005
@@ -107,4 +107,23 @@ quit
 ****
 #
 #
-# Need to additionally test the timeout / defer_ok case
+#
+#
+server -i 2 DIR/eximdir/clam_sock
+<SCAN
+>LF>scanned_file_name: OK
+<*eof
+****
+#
+exim -odi -bs -DCONTROL="retry=4s"
+ehlo test.ex
+mail from:<>
+rcpt to:<userx@???>
+data
+Date: Fri, 17 Dec 2004 14:35:01 +0100
+Subject: message should be accepted after a retry
+
+.
+quit
+****
+#
diff --git a/test/scripts/4000-scanning/4009 b/test/scripts/4000-scanning/4009
index 8e34c98..4c2ab81 100644
--- a/test/scripts/4000-scanning/4009
+++ b/test/scripts/4000-scanning/4009
@@ -1,4 +1,7 @@
 # content scan interface: spamassassin
+#
+# A good-comms test, returning not-spam.
+# (we could use a second one that returns is-spam...)
 server 7833
 <REPORT SPAMC
 <User:
@@ -53,3 +56,123 @@ test
 .
 quit
 ****
+#
+#
+#
+#
+# Server spec line with timeout option, not exercised
+# (could we cut down the massive content?)
+server 7833
+<REPORT SPAMC
+<User:
+<Content-length:
+<
+<From 
+<X-Envelope-From
+<X-Envelope-To
+<Received: 
+<    by 
+<    (envelope
+<    id 
+<    for 
+<Content-type: text/plain
+<Message-Id:
+<From:
+<Date:
+<
+<test
+>SPAMD/1.1 0 EX_OK
+>Spam: False ; 4.5 / 5.0
+>
+>Spam detection software, running on the system "demo",
+>has NOT identified this incoming email as spam.  The original
+>message has been attached to this so you can view it or label
+>similar future email.  If you have any questions, see
+>@@CONTACT_ADDRESS@@ for details.
+>
+>Content preview:  test [...] 
+>
+>Content analysis details:   (4.5 points, 5.0 required)
+>
+> pts rule name              description
+>---- ---------------------- --------------------------------------------------
+>-1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP
+> 1.2 MISSING_HEADERS        Missing To: header
+> 1.0 MISSING_FROM           Missing From: header
+> 1.8 MISSING_SUBJECT        Missing Subject: header
+> 1.4 MISSING_DATE           Missing Date: header
+> 0.1 MISSING_MID            Missing Message-Id: header
+>
+*eof
+****
+exim -odi -bs -DOPT='retry=10s'
+ehlo test.ex
+mail from:<>
+rcpt to:<userx@???>
+data
+Content-type: text/plain
+
+test
+.
+quit
+****
+#
+#
+#
+# Server spec line with timeout option, exercised
+server -i 2 7833
+<REPORT SPAMC
+<User:
+<Content-length:
+<
+<From 
+<X-Envelope-From
+<X-Envelope-To
+<Received: 
+<    by 
+<    (envelope
+<    id 
+<    for 
+<Content-type: text/plain
+<Message-Id:
+<From:
+<Date:
+<
+<test
+>SPAMD/1.1 0 EX_OK
+>Spam: False ; 4.5 / 5.0
+>
+>Spam detection software, running on the system "demo",
+>has NOT identified this incoming email as spam.  The original
+>message has been attached to this so you can view it or label
+>similar future email.  If you have any questions, see
+>@@CONTACT_ADDRESS@@ for details.
+>
+>Content preview:  test [...] 
+>
+>Content analysis details:   (4.5 points, 5.0 required)
+>
+> pts rule name              description
+>---- ---------------------- --------------------------------------------------
+>-1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP
+> 1.2 MISSING_HEADERS        Missing To: header
+> 1.0 MISSING_FROM           Missing From: header
+> 1.8 MISSING_SUBJECT        Missing Subject: header
+> 1.4 MISSING_DATE           Missing Date: header
+> 0.1 MISSING_MID            Missing Message-Id: header
+>
+*eof
+****
+exim -odi -bs -DOPT='retry=4s'
+ehlo test.ex
+mail from:<>
+rcpt to:<userx@???>
+data
+Content-type: text/plain
+
+test
+.
+quit
+****
+#
+#
diff --git a/test/src/server.c b/test/src/server.c
index 0d6e5fe..f4173ec 100644
--- a/test/src/server.c
+++ b/test/src/server.c
@@ -143,6 +143,7 @@ int connection_count = 1;
 int count;
 int on = 1;
 int timeout = 5;
+int initial_pause = 0;
 int use_ipv4 = 1;
 int use_ipv6 = 1;
 int debug = 0;
@@ -180,6 +181,7 @@ while (na < argc && argv[na][0] == '-')
   {
   if (strcmp(argv[na], "-d") == 0) debug = 1;
   else if (strcmp(argv[na], "-t") == 0) timeout = atoi(argv[++na]);
+  else if (strcmp(argv[na], "-i") == 0) initial_pause = atoi(argv[++na]);
   else if (strcmp(argv[na], "-noipv4") == 0) use_ipv4 = 0;
   else if (strcmp(argv[na], "-noipv6") == 0) use_ipv6 = 0;
   else
@@ -213,11 +215,22 @@ na++;
 if (na < argc) connection_count = atoi(argv[na]);



+/* Initial pause (before creating listen sockets */
+if (initial_pause > 0)
+  {
+  if (debug)
+    printf("%d: Inital pause of %d seconds\n", time(NULL), initial_pause);
+  else
+    printf("Inital pause of %d seconds\n", initial_pause);
+  while (initial_pause > 0)
+    initial_pause = sleep(initial_pause);
+  }
+
 /* Create sockets */


 if (port == 0)  /* Unix domain */
   {
-  if (debug) printf("Creating Unix domain socket\n");
+  if (debug) printf("%d: Creating Unix domain socket\n", time(NULL));
   listen_socket[udn] = socket(PF_UNIX, SOCK_STREAM, 0);
   if (listen_socket[udn] < 0)
     {
diff --git a/test/stdout/4005 b/test/stdout/4005
index 0a8a5ea..4d858c5 100644
--- a/test/stdout/4005
+++ b/test/stdout/4005
@@ -53,6 +53,17 @@
 354 Enter message, ending with "." on a line by itself
 250 OK id=10HmaZ-0005vi-00
 221 myhost.test.ex closing connection
+220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000
+250-myhost.test.ex Hello CALLER at test.ex
+250-SIZE 52428800
+250-8BITMIME
+250-PIPELINING
+250 HELP
+250 OK
+250 Accepted
+354 Enter message, ending with "." on a line by itself
+250 OK id=10HmbC-0005vi-00
+221 myhost.test.ex closing connection


******** SERVER ********
Listening on TESTSUITE/eximdir/clam_sock ...
@@ -81,3 +92,10 @@ Listening on TESTSUITE/eximdir/clam_sock ...
Connection request
*sleep 3
End of script
+Inital pause of 2 seconds
+Listening on TESTSUITE/eximdir/clam_sock ...
+Connection request
+<SCAN TESTSUITE/spool/scan/10HmbC-0005vi-00/10HmbC-0005vi-00.eml
+>LF>scanned_file_name: OK
+Unexpected EOF read from client
+End of script
diff --git a/test/stdout/4009 b/test/stdout/4009
index a1d7f2e..9220c7d 100644
--- a/test/stdout/4009
+++ b/test/stdout/4009
@@ -9,6 +9,28 @@
354 Enter message, ending with "." on a line by itself
250 OK id=10HmaX-0005vi-00
221 myhost.test.ex closing connection
+220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000
+250-myhost.test.ex Hello CALLER at test.ex
+250-SIZE 52428800
+250-8BITMIME
+250-PIPELINING
+250 HELP
+250 OK
+250 Accepted
+354 Enter message, ending with "." on a line by itself
+250 OK id=10HmaY-0005vi-00
+221 myhost.test.ex closing connection
+220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000
+250-myhost.test.ex Hello CALLER at test.ex
+250-SIZE 52428800
+250-8BITMIME
+250-PIPELINING
+250 HELP
+250 OK
+250 Accepted
+354 Enter message, ending with "." on a line by itself
+250 OK id=10HmaZ-0005vi-00
+221 myhost.test.ex closing connection

******** SERVER ********
Listening on port 7833 ...
@@ -55,3 +77,92 @@ Connection request from [127.0.0.1]
>

 Expected EOF read from client
 End of script
+Listening on port 7833 ... 
+Connection request from [127.0.0.1]
+<REPORT SPAMC/1.2
+<User: nobody
+<Content-length: 479
+<
+<From MAILER-DAEMON Tue Mar 02 09:44:33 1999
+<X-Envelope-From: <CALLER@???>
+<X-Envelope-To: userx@???
+<Received: from CALLER (helo=test.ex)
+<    by myhost.test.ex with local-esmtp (Exim x.yz)
+<    (envelope-from <CALLER@???>)
+<    id 10HmaY-0005vi-00
+<    for userx@???; Tue, 2 Mar 1999 09:44:33 +0000
+<Content-type: text/plain
+<Message-Id: <E10HmaY-0005vi-00@???>
+<From: CALLER_NAME <CALLER@???>
+<Date: Tue, 2 Mar 1999 09:44:33 +0000
+<
+<test
+>SPAMD/1.1 0 EX_OK
+>Spam: False ; 4.5 / 5.0
+>
+>Spam detection software, running on the system "demo",
+>has NOT identified this incoming email as spam.  The original
+>message has been attached to this so you can view it or label
+>similar future email.  If you have any questions, see
+>@@CONTACT_ADDRESS@@ for details.
+>
+>Content preview:  test [...]
+>
+>Content analysis details:   (4.5 points, 5.0 required)
+>
+> pts rule name              description
+>---- ---------------------- --------------------------------------------------
+>-1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP
+> 1.2 MISSING_HEADERS        Missing To: header
+> 1.0 MISSING_FROM           Missing From: header
+> 1.8 MISSING_SUBJECT        Missing Subject: header
+> 1.4 MISSING_DATE           Missing Date: header
+> 0.1 MISSING_MID            Missing Message-Id: header
+>
+Expected EOF read from client
+End of script
+Inital pause of 2 seconds
+Listening on port 7833 ... 
+Connection request from [127.0.0.1]
+<REPORT SPAMC/1.2
+<User: nobody
+<Content-length: 479
+<
+<From MAILER-DAEMON Tue Mar 02 09:44:33 1999
+<X-Envelope-From: <CALLER@???>
+<X-Envelope-To: userx@???
+<Received: from CALLER (helo=test.ex)
+<    by myhost.test.ex with local-esmtp (Exim x.yz)
+<    (envelope-from <CALLER@???>)
+<    id 10HmaZ-0005vi-00
+<    for userx@???; Tue, 2 Mar 1999 09:44:33 +0000
+<Content-type: text/plain
+<Message-Id: <E10HmaZ-0005vi-00@???>
+<From: CALLER_NAME <CALLER@???>
+<Date: Tue, 2 Mar 1999 09:44:33 +0000
+<
+<test
+>SPAMD/1.1 0 EX_OK
+>Spam: False ; 4.5 / 5.0
+>
+>Spam detection software, running on the system "demo",
+>has NOT identified this incoming email as spam.  The original
+>message has been attached to this so you can view it or label
+>similar future email.  If you have any questions, see
+>@@CONTACT_ADDRESS@@ for details.
+>
+>Content preview:  test [...]
+>
+>Content analysis details:   (4.5 points, 5.0 required)
+>
+> pts rule name              description
+>---- ---------------------- --------------------------------------------------
+>-1.0 ALL_TRUSTED            Passed through trusted hosts only via SMTP
+> 1.2 MISSING_HEADERS        Missing To: header
+> 1.0 MISSING_FROM           Missing From: header
+> 1.8 MISSING_SUBJECT        Missing Subject: header
+> 1.4 MISSING_DATE           Missing Date: header
+> 0.1 MISSING_MID            Missing Message-Id: header
+>
+Expected EOF read from client
+End of script