0.9.6claws4
[claws.git] / src / plugins / spamassassin / libspamc.c
index c870ad31f43fb0e47bbb91889e419632a240f5c4..84d21da41246121e33dd7e2b74ac7b14a546b4c7 100644 (file)
@@ -6,28 +6,23 @@
  * "License".
  */
 
-#include "../config.h"
+#include "config.h"
 #include "libspamc.h"
 #include "utils.h"
 
 #include <unistd.h>
 #include <stdlib.h>
+#include <assert.h>
 #include <stdio.h>
 #include <string.h>
 #include <syslog.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
+#include <sys/un.h>
 #include <netinet/tcp.h>
 #include <arpa/inet.h>
 
-#ifdef SPAMC_SSL
-#include <openssl/crypto.h>
-#include <openssl/pem.h>
-#include <openssl/ssl.h>
-#include <openssl/err.h>
-#endif
-
 #ifdef HAVE_SYSEXITS_H
 #include <sysexits.h>
 #endif
 
 /* RedHat 5.2 doesn't define Shutdown 2nd Parameter Constants */
 /* KAM 12-4-01 */
-#ifndef HAVE_SHUT_RD
-#define SHUT_RD (0)   /* No more receptions.  */
-#define SHUT_WR (1)   /* No more transmissions.  */
-#define SHUT_RDWR (2) /* No more receptions or transmissions.  */
+/* SJF 2003/04/25 - now test for macros directly */
+#ifndef SHUT_RD
+#  define SHUT_RD 0    /* no more receptions */
+#endif
+#ifndef SHUT_WR
+#  define SHUT_WR 1    /* no more transmissions */
+#endif
+#ifndef SHUT_RDWR
+#  define SHUT_RDWR 2  /* no more receptions or transmissions */
 #endif
 
 #ifndef HAVE_H_ERRNO
@@ -75,7 +75,8 @@ extern char *optarg;
 #define EX__MAX 200 
 #endif
 
-static const int DO_CONNECT_DEBUG_SYSLOGS = 0;
+#undef DO_CONNECT_DEBUG_SYSLOGS
+/* or #define DO_CONNECT_DEBUG_SYSLOGS 1 */
 
 static const int ESC_PASSTHROUGHRAW = EX__MAX+666;
 
@@ -90,10 +91,306 @@ static const int EXPANSION_ALLOWANCE = 16384;
  */
 
 /* Set the protocol version that this spamc speaks */
-static const char *PROTOCOL_VERSION="SPAMC/1.2";
+static const char *PROTOCOL_VERSION="SPAMC/1.3";
+
+/* "private" part of struct message.
+ * we use this instead of the struct message directly, so that we
+ * can add new members without affecting the ABI.
+ */
+struct libspamc_private_message {
+  int flags;   /* copied from "flags" arg to message_read() */
+};
 
 int libspamc_timeout = 0;
 
+/*
+ * translate_connect_errno()
+ *
+ *     Given a UNIX error number obtained (probably) from "connect(2)",
+ *     translate this to a failure code. This module is shared by both
+ *     transport modules - UNIX and TCP.
+ *
+ *     This should ONLY be called when there is an error.
+ */
+static int
+translate_connect_errno(int err)
+{
+       switch (err)
+       {
+         case EBADF:
+         case EFAULT:
+         case ENOTSOCK:
+         case EISCONN:
+         case EADDRINUSE:
+         case EINPROGRESS:
+         case EALREADY:
+         case EAFNOSUPPORT:
+               return EX_SOFTWARE;
+
+         case ECONNREFUSED:
+         case ETIMEDOUT:
+         case ENETUNREACH:
+               return EX_UNAVAILABLE;
+
+         case EACCES:
+               return EX_NOPERM;
+
+         default:
+               return EX_SOFTWARE;
+       }
+}
+
+/*
+ * opensocket()
+ *
+ *     Given a socket type (PF_INET or PF_UNIX), try to create this socket
+ *     and store the FD in the pointed-to place. If it's successful, do any
+ *     other setup required to make the socket ready to use, such as setting
+ *     TCP_NODELAY mode, and in any case we return EX_OK if all is well.
+ *
+ *     Upon failure we return one of the other EX_??? error codes.
+ */
+static int opensocket(int type, int *psock)
+{
+const char *typename;
+int        proto = 0;
+
+       assert(psock != 0);
+
+       /*----------------------------------------------------------------
+        * Create a few induction variables that are implied by the socket
+        * type given by the user. The typename is strictly used for debug
+        * reporting.
+        */
+       if ( type == PF_UNIX )
+       {
+               typename = "PF_UNIX";
+       }
+       else
+       {
+               typename = "PF_INET";
+               proto    = IPPROTO_TCP;
+       }
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+       syslog (DEBUG_LEVEL, "dbg: create socket(%s)", typename);
+#endif
+
+       if ( (*psock = socket(type, SOCK_STREAM, proto)) < 0 )
+       {
+       int     origerr;
+
+               /*--------------------------------------------------------
+                * At this point we had a failure creating the socket, and
+                * this is pretty much fatal. Translate the error reason
+                * into something the user can understand.
+                */
+               origerr = errno;    /* take a copy before syslog() */
+
+               syslog (LOG_ERR, "socket(%s) to spamd failed: %m", typename);
+
+               switch (origerr)
+               {
+                 case EPROTONOSUPPORT:
+                 case EINVAL:
+                       return EX_SOFTWARE;
+
+                 case EACCES:
+                       return EX_NOPERM;
+
+                 case ENFILE:
+                 case EMFILE:
+                 case ENOBUFS:
+                 case ENOMEM:
+                       return EX_OSERR;
+
+                 default:
+                       return EX_SOFTWARE;
+               }
+       }
+
+
+       /*----------------------------------------------------------------
+        * Do a bit of setup on the TCP socket if required. Notes above
+        * suggest this is probably not set
+        */
+#ifdef USE_TCP_NODELAY
+       {
+               int one = 1;
+
+               if ( type == PF_INET
+                &&  setsockopt(*psock, 0, TCP_NODELAY, &one, sizeof one) != 0 )
+               {
+                       switch(errno)
+                       {
+                         case EBADF:
+                         case ENOTSOCK:
+                         case ENOPROTOOPT:
+                         case EFAULT:
+                               syslog(LOG_ERR,
+                                  "setsockopt(TCP_NODELAY) failed: %m");
+                               close (*psock);
+                               return EX_SOFTWARE;
+
+                         default:
+                               break;            /* ignored */
+                       }
+               }
+       }
+#endif /* USE_TCP_NODELAY */
+
+       return EX_OK;   /* all is well */
+}
+
+/*
+ * try_to_connect_unix()
+ *
+ *     Given a transport handle that implies using a UNIX domain
+ *     socket, try to make a connection to it and store the resulting
+ *     file descriptor in *sockptr. Return is EX_OK if we did it,
+ *     and some other error code otherwise.
+ */
+static int
+try_to_connect_unix (struct transport *tp, int *sockptr)
+{
+int mysock, status, origerr;
+struct sockaddr_un addrbuf;
+int ret;
+
+       assert(tp             != 0);
+       assert(sockptr        != 0);
+       assert(tp->socketpath != 0);
+
+       /*----------------------------------------------------------------
+        * If the socket itself can't be created, this is a fatal error.
+        */
+       if ( (ret = opensocket(PF_UNIX, &mysock)) != EX_OK )
+               return ret;
+
+       /* set up the UNIX domain socket */
+       memset(&addrbuf, 0, sizeof addrbuf);
+       addrbuf.sun_family = AF_UNIX;
+       strncpy(addrbuf.sun_path, tp->socketpath, sizeof addrbuf.sun_path - 1);
+       addrbuf.sun_path[sizeof addrbuf.sun_path - 1] = '\0';
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+       syslog (DEBUG_LEVEL, "dbg: connect(AF_UNIX) to spamd at %s",
+               addrbuf.sun_path);
+#endif
+
+       status = connect(mysock, (struct sockaddr *) &addrbuf, sizeof(addrbuf));
+
+       origerr = errno;
+
+       if ( status >= 0 )
+       {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+               syslog(DEBUG_LEVEL, "dbg: connect(AF_UNIX) ok");
+#endif
+
+               *sockptr = mysock;
+
+               return EX_OK;
+       }
+
+       syslog(LOG_ERR, "connect(AF_UNIX) to spamd %s failed: %m",
+               addrbuf.sun_path);
+
+       close(mysock);
+
+       return translate_connect_errno(origerr);
+}
+
+/*
+ * try_to_connect_tcp()
+ *
+ *     Given a transport that implies a TCP connection, either to
+ *     localhost or a list of IP addresses, attempt to connect. The
+ *     list of IP addresses has already been randomized (if requested)
+ *     and limited to just one if fallback has been enabled.
+ */
+static int
+try_to_connect_tcp (const struct transport *tp, int *sockptr)
+{
+int    numloops;
+int    origerr = 0;
+int    ret;
+
+       assert(tp        != 0);
+       assert(sockptr   != 0);
+       assert(tp->nhosts > 0);
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+       for (numloops = 0; numloops < tp->nhosts; numloops++)
+       {
+               syslog(LOG_ERR, "dbg: %d/%d: %s",
+                       numloops+1, tp->nhosts, inet_ntoa(tp->hosts[numloops]));
+       }
+#endif
+
+       for (numloops = 0; numloops < MAX_CONNECT_RETRIES; numloops++)
+       {
+       struct sockaddr_in addrbuf;
+       const int          hostix = numloops % tp->nhosts;
+       int                status, mysock;
+       const char       * ipaddr;
+
+               /*--------------------------------------------------------
+                * We always start by creating the socket, as we get only
+                * one attempt to connect() on each one. If this fails,
+                * we're done.
+                */
+               if ( (ret = opensocket(PF_INET, &mysock)) != EX_OK )
+                       return ret;
+
+               memset(&addrbuf, 0, sizeof(addrbuf));
+
+               addrbuf.sin_family = AF_INET;
+               addrbuf.sin_port   = htons(tp->port);
+               addrbuf.sin_addr   = tp->hosts[hostix];
+
+               ipaddr = inet_ntoa(addrbuf.sin_addr);
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+               syslog (DEBUG_LEVEL,
+                       "dbg: connect(AF_INET) to spamd at %s (try #%d of %d)",
+                       ipaddr,
+                       numloops+1,
+                       MAX_CONNECT_RETRIES);
+#endif
+
+               status = connect(mysock, (struct sockaddr *)&addrbuf, sizeof(addrbuf));
+
+               if (status != 0)
+               {
+                       syslog (LOG_ERR,
+                       "connect(AF_INET) to spamd at %s failed, retrying (#%d of %d): %m",
+                               ipaddr, numloops+1, MAX_CONNECT_RETRIES);
+
+                       close(mysock);
+
+                       sleep(CONNECT_RETRY_SLEEP);
+               }
+               else
+               {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+                       syslog(DEBUG_LEVEL,
+                               "dbg: connect(AF_INET) to spamd at %s done",
+                               ipaddr);
+#endif
+                       *sockptr = mysock;
+
+                       return EX_OK;
+               }
+       }
+
+       syslog (LOG_ERR, "connection attempt to spamd aborted after %d retries",
+               MAX_CONNECT_RETRIES);
+
+       return translate_connect_errno(origerr);
+}
+
+#if 0
 static int
 try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
                 int hent_port, int *sockptr)
@@ -109,7 +406,9 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
   struct sockaddr_in addrbuf, *addr;
   struct in_addr inaddrlist[256];
 
-  int i; char dbgbuf[2048]; int dbgbuflen = 0;         // DBG
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+  int dbgiter; char dbgbuf[2048]; int dbgbuflen = 0;
+#endif
 
   /* NOTE: do not call syslog() (unless you are about to return) before
    * we take a copy of the h_addr_list.
@@ -129,6 +428,8 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
     memset (inaddrlist, 0, sizeof(inaddrlist));
 
     for (hostnum=0; hent->h_addr_list[hostnum] != 0; hostnum++) {
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
       dbgbuflen += snprintf (dbgbuf+dbgbuflen, 2047-dbgbuflen,
                  "[%d %lx: %d.%d.%d.%d]",
                  hostnum, hent->h_addr_list[hostnum],
@@ -136,6 +437,7 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
                  hent->h_addr_list[hostnum][1],
                  hent->h_addr_list[hostnum][2],
                  hent->h_addr_list[hostnum][3]);
+#endif
 
       if (hostnum > 255) {
        syslog (LOG_ERR, "too many address in hostent (%d), ignoring others",
@@ -149,34 +451,38 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
        return EX_SOFTWARE;
       }
 
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
       dbgbuflen += snprintf (dbgbuf+dbgbuflen, 2047-dbgbuflen,
                  "[%d: %d.%d.%d.%d] ", sizeof (struct in_addr),
                  hent->h_addr_list[hostnum][0],
                  hent->h_addr_list[hostnum][1],
                  hent->h_addr_list[hostnum][2],
                  hent->h_addr_list[hostnum][3]);
+#endif
 
       memcpy ((void *) &(inaddrlist[hostnum]),
                (void *) hent->h_addr_list[hostnum],
                sizeof (struct in_addr));
     }
 
-    if (DO_CONNECT_DEBUG_SYSLOGS) {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
       syslog (LOG_DEBUG, "dbg: %d %s", hostnum, dbgbuf); dbgbuflen = 0;
-    }
+#endif
   }
 
 
-  if (DO_CONNECT_DEBUG_SYSLOGS) {
-    for (i = 0; i < hostnum; i++) {
-      syslog (LOG_DEBUG, "dbg: host addr %d/%d = %lx at %lx",
-                 i, hostnum, inaddrlist[i].s_addr, &(inaddrlist[i]));
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+    for (dbgiter = 0; dbgiter < hostnum; dbgiter++) {
+      syslog (LOG_DEBUG, "dbg: host addr %d/%d = %lx at %lx", dbgiter, hostnum,
+                 inaddrlist[dbgiter].s_addr, &(inaddrlist[dbgiter]));
     }
-  }
+#endif
 
   hent = NULL; /* cannot use hent after this point, syslog() may overwrite it */
 
-  if (DO_CONNECT_DEBUG_SYSLOGS) { syslog (LOG_DEBUG, "dbg: socket"); }
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
+  syslog (LOG_DEBUG, "dbg: socket");
+#endif
 
   if(-1 == (mysock = socket(PF_INET,SOCK_STREAM,0)))
   {
@@ -198,8 +504,6 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
       return EX_SOFTWARE;
     }
   }
-  
-  if (DO_CONNECT_DEBUG_SYSLOGS) { syslog (LOG_DEBUG, "dbg: setsockopt"); }
 
 #ifdef USE_TCP_NODELAY
   /* TODO: should this be up above the connect()? */
@@ -223,14 +527,17 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
 #endif
 
   for (numloops=0; numloops < MAX_CONNECT_RETRIES; numloops++) {
-    if (DO_CONNECT_DEBUG_SYSLOGS) {
+  
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
       syslog (LOG_DEBUG, "dbg: connect() to spamd %d", numloops);
-    }
+#endif
+
     if (argaddr != NULL) {
       addr = (struct sockaddr_in *) argaddr;     /* use the one provided */
-      if (DO_CONNECT_DEBUG_SYSLOGS) {
+  
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
        syslog (LOG_DEBUG, "dbg: using argaddr");
-      }
+#endif
 
     } else {
       /* cycle through the addrs in hent */
@@ -244,29 +551,32 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
        return EX_SOFTWARE;
       }
 
-      if (DO_CONNECT_DEBUG_SYSLOGS) {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
        syslog (LOG_DEBUG, "dbg: cpy addr %d/%d at %lx",
                numloops%hostnum, hostnum, &(inaddrlist[numloops % hostnum]));
-      }
+#endif
 
       memcpy (&addrbuf.sin_addr, &(inaddrlist[numloops % hostnum]),
                         sizeof(addrbuf.sin_addr));
       addr = &addrbuf;
 
-      if (DO_CONNECT_DEBUG_SYSLOGS) {
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
        syslog (LOG_DEBUG, "dbg: conn addr %d/%d = %lx",
            numloops%hostnum, hostnum, addrbuf.sin_addr.s_addr);
-      }
+#endif
 
     }
 
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
     syslog (LOG_DEBUG, "dbg: connect() to spamd at %s",
                inet_ntoa(((struct sockaddr_in *)addr)->sin_addr));
+#endif
     status = connect(mysock,(const struct sockaddr *) addr, sizeof(*addr));
-    if (DO_CONNECT_DEBUG_SYSLOGS) {
+
+#ifdef DO_CONNECT_DEBUG_SYSLOGS
       syslog (LOG_DEBUG, "dbg: connect() to spamd at %s done",
          inet_ntoa(((struct sockaddr_in *)addr)->sin_addr));
-    }
+#endif
 
     if (status < 0)
     {
@@ -308,6 +618,7 @@ try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
      return EX_SOFTWARE;
   }
 }
+#endif
 
 /* Aug 14, 2002 bj: Reworked things. Now we have message_read, message_write,
  * message_dump, lookup_host, message_filter, and message_process, and a bunch
@@ -323,12 +634,14 @@ static void clear_message(struct message *m){
     m->is_spam=EX_TOOBIG;
     m->score=0.0; m->threshold=0.0;
     m->out=NULL; m->out_len=0;
+    m->content_length=-1;
 }
 
-static int message_read_raw(int fd, struct message *m){
+static int
+message_read_raw(int fd, struct message *m){
     clear_message(m);
     if((m->raw=malloc(m->max_len+1))==NULL) return EX_OSERR;
-    m->raw_len=full_read(fd, (unsigned char *) m->raw, m->max_len+1, m->max_len+1);
+    m->raw_len=full_read(fd, m->raw, m->max_len+1, m->max_len+1);
     if(m->raw_len<=0){
         free(m->raw); m->raw=NULL; m->raw_len=0;
         return EX_IOERR;
@@ -351,7 +664,7 @@ static int message_read_bsmtp(int fd, struct message *m){
     if((m->raw=malloc(m->max_len+1))==NULL) return EX_OSERR;
 
     /* Find the DATA line */
-    m->raw_len=full_read(fd, (unsigned char *) m->raw, m->max_len+1, m->max_len+1);
+    m->raw_len=full_read(fd, m->raw, m->max_len+1, m->max_len+1);
     if(m->raw_len<=0){
         free(m->raw); m->raw=NULL; m->raw_len=0;
         return EX_IOERR;
@@ -407,6 +720,14 @@ static int message_read_bsmtp(int fd, struct message *m){
 int message_read(int fd, int flags, struct message *m){
     libspamc_timeout = 0;
 
+    /* create the "private" part of the struct message */
+    m->priv = malloc (sizeof (struct libspamc_private_message));
+    if (m->priv == NULL) {
+        syslog(LOG_ERR, "message_read: malloc failed");
+        return EX_OSERR;
+    }
+    m->priv->flags = flags;
+
     switch(flags&SPAMC_MODE_MASK){
       case SPAMC_RAW_MODE:
         return message_read_raw(fd, m);
@@ -426,23 +747,30 @@ long message_write(int fd, struct message *m){
     off_t jlimit;
     char buffer[1024];
 
-    if(m->is_spam==EX_ISSPAM || m->is_spam==EX_NOTSPAM){
-        return full_write(fd, (unsigned char *) m->out, m->out_len);
+    if (m->priv->flags&SPAMC_CHECK_ONLY) {
+       if(m->is_spam==EX_ISSPAM || m->is_spam==EX_NOTSPAM){
+           return full_write(fd, m->out, m->out_len);
+
+       } else {
+           syslog(LOG_ERR, "oops! SPAMC_CHECK_ONLY is_spam: %d\n", m->is_spam);
+           return -1;
+       }
     }
 
+    /* else we're not in CHECK_ONLY mode */
     switch(m->type){
       case MESSAGE_NONE:
         syslog(LOG_ERR, "Cannot write this message, it's MESSAGE_NONE!\n");
         return -1;
 
       case MESSAGE_ERROR:
-        return full_write(fd, (unsigned char *) m->raw, m->raw_len);
+        return full_write(fd, m->raw, m->raw_len);
 
       case MESSAGE_RAW:
-        return full_write(fd, (unsigned char *) m->out, m->out_len);
+        return full_write(fd, m->out, m->out_len);
 
       case MESSAGE_BSMTP:
-        total=full_write(fd, (unsigned char *) m->pre, m->pre_len);
+        total=full_write(fd, m->pre, m->pre_len);
         for(i=0; i<m->out_len; ){
            jlimit = (off_t) (sizeof(buffer)/sizeof(*buffer)-4);
             for(j=0; i < (off_t) m->out_len &&
@@ -459,9 +787,9 @@ long message_write(int fd, struct message *m){
                     buffer[j++]=m->out[i++];
                 }
             }
-            total+=full_write(fd, (unsigned char *) buffer, j);
+            total+=full_write(fd, buffer, j);
         }
-        return total+full_write(fd, (unsigned char *) m->post, m->post_len);
+        return total+full_write(fd, m->post, m->post_len);
 
       default:
         syslog(LOG_ERR, "Unknown message type %d\n", m->type);
@@ -476,45 +804,202 @@ void message_dump(int in_fd, int out_fd, struct message *m){
     if(m!=NULL && m->type!=MESSAGE_NONE) {
         message_write(out_fd, m);
     }
-    while((bytes=full_read(in_fd, (unsigned char *) buf, 8192, 8192))>0){
-        if (bytes!=full_write(out_fd, (unsigned char *) buf, bytes)) {
+    while((bytes=full_read(in_fd, buf, 8192, 8192))>0){
+        if (bytes!=full_write(out_fd, buf, bytes)) {
             syslog(LOG_ERR, "oops! message_dump of %d returned different", bytes);
         }
     }
 }
 
-static int _message_filter(const struct sockaddr *addr,
-                const struct hostent *hent, int hent_port, char *username,
+static int
+_spamc_read_full_line (struct message *m, int flags, SSL *ssl, int sock,
+               char *buf, int *lenp, int bufsiz)
+{
+    int failureval;
+    int bytesread = 0;
+    int len;
+
+    UNUSED_VARIABLE(m);
+
+    /* Now, read from spamd */
+    for(len=0; len<bufsiz-1; len++) {
+       if(flags&SPAMC_USE_SSL) {
+         bytesread = ssl_timeout_read (ssl, buf+len, 1);
+       } else {
+         bytesread = fd_timeout_read (sock, buf+len, 1);
+       }
+
+        if(buf[len]=='\n') {
+            buf[len]='\0';
+           if (len > 0 && buf[len-1] == '\r') {
+               len--;
+               buf[len]='\0';
+           }
+           *lenp = len;
+           return EX_OK;
+       }
+
+        if(bytesread<=0){
+           failureval = EX_IOERR; goto failure;
+        }
+    }
+
+    syslog(LOG_ERR, "spamd responded with line of %d bytes, dying", len);
+    failureval = EX_TOOBIG;
+
+failure:
+    return failureval;
+}
+
+/*
+ * May  7 2003 jm: using %f is bad where LC_NUMERIC is "," in the locale.
+ * work around using our own locale-independent float-parser code.
+ */
+static float
+_locale_safe_string_to_float (char *buf, int siz)
+{
+  int is_neg;
+  char *cp, *dot;
+  int divider;
+  float ret, postdot;
+
+  buf[siz-1] = '\0';   /* ensure termination */
+
+  /* ok, let's illustrate using "100.033" as an example... */
+  
+  is_neg = 0;
+  if (*buf == '-') { is_neg = 1; }
+
+  ret = (float) (strtol (buf, &dot, 10));
+  if (dot == NULL) { return 0.0; }
+  if (dot != NULL && *dot != '.') { return ret; }
+
+  /* ex: ret == 100.0 */
+
+  cp = (dot + 1);
+  postdot = (float) (strtol (cp, NULL, 10));
+  if (postdot == 0.0) { return ret; }
+
+  /* ex: postdot == 33.0, cp="033" */
+
+  /* now count the number of decimal places and figure out what power of 10 to use */
+  divider = 1;
+  while (*cp != '\0') {
+    divider *= 10; cp++;
+  }
+
+  /* ex:
+   * cp="033", divider=1
+   * cp="33", divider=10
+   * cp="3", divider=100
+   * cp="", divider=1000
+   */
+
+  if (is_neg) {
+    ret -= (postdot / ((float) divider));
+  } else {
+    ret += (postdot / ((float) divider));
+  }
+  /* ex: ret == 100.033, tada! ... hopefully */
+
+  return ret;
+}
+
+static int
+_handle_spamd_header (struct message *m, int flags, char *buf, int len)
+{
+    char is_spam[6];
+    char s_str[21], t_str[21];
+
+    UNUSED_VARIABLE(len);
+
+    /* Feb 12 2003 jm: actually, I think sccanf is working fine here ;)
+     * let's stick with it for this parser.
+     * May  7 2003 jm: using %f is bad where LC_NUMERIC is "," in the locale.
+     * work around using our own locale-independent float-parser code.
+     */
+    if (sscanf(buf, "Spam: %5s ; %20s / %20s", is_spam, s_str, t_str) == 3)
+    {
+       m->score = _locale_safe_string_to_float (s_str, 20);
+       m->threshold = _locale_safe_string_to_float (t_str, 20);
+
+       /* Format is "Spam: x; y / x" */
+       m->is_spam=strcasecmp("true", is_spam) == 0 ? EX_ISSPAM: EX_NOTSPAM;
+
+       if(flags&SPAMC_CHECK_ONLY) {
+           m->out_len=snprintf (m->out, m->max_len+EXPANSION_ALLOWANCE,
+                       "%.1f/%.1f\n", m->score, m->threshold);
+       }
+       else if ((flags & SPAMC_REPORT_IFSPAM && m->is_spam == EX_ISSPAM)
+               || (flags & SPAMC_REPORT))
+       {
+           m->out_len=snprintf (m->out, m->max_len+EXPANSION_ALLOWANCE,
+                       "%.1f/%.1f\n", m->score, m->threshold);
+       }
+       return EX_OK;
+
+    } else if(sscanf(buf, "Content-length: %d", &m->content_length) == 1) {
+       if (m->content_length < 0) {
+           syslog(LOG_ERR, "spamd responded with bad Content-length '%s'", buf);
+           return EX_PROTOCOL;
+       }
+       return EX_OK;
+    }
+
+    syslog(LOG_ERR, "spamd responded with bad header '%s'", buf);
+    return EX_PROTOCOL;
+}
+
+int message_filter(struct transport *tp, const char *username,
                 int flags, struct message *m)
 {
-    char buf[8192], is_spam[6];
+    char buf[8192];
     int bufsiz = (sizeof(buf) / sizeof(*buf)) - 4; /* bit of breathing room */
-    int len, expected_len, i, header_read=0;
-    int sock;
+    int len, i;
+    int sock = -1;
+    int rc;
+    char versbuf[20];
     float version;
     int response;
     int failureval;
-#ifdef SPAMC_SSL
     SSL_CTX* ctx;
     SSL* ssl;
     SSL_METHOD *meth;
 
-    if(flags&SPAMC_USE_SSL){   
+    if (flags&SPAMC_USE_SSL) {
+#ifdef SPAMC_SSL
       SSLeay_add_ssl_algorithms();
       meth = SSLv2_client_method();
       SSL_load_error_strings();
       ctx = SSL_CTX_new(meth);
-    }    
+#else
+      UNUSED_VARIABLE(ssl);
+      UNUSED_VARIABLE(meth);
+      UNUSED_VARIABLE(ctx);
+      syslog(LOG_ERR, "spamc not built with SSL support");
+      return EX_SOFTWARE;
 #endif
+    }    
 
     m->is_spam=EX_TOOBIG;
     if((m->out=malloc(m->max_len+EXPANSION_ALLOWANCE+1))==NULL){
-        return EX_OSERR;
+        failureval = EX_OSERR; goto failure;
     }
     m->out_len=0;
 
+
     /* Build spamd protocol header */
-    len=snprintf(buf, bufsiz, "%s %s\r\n", (flags&SPAMC_CHECK_ONLY)?"CHECK":"PROCESS", PROTOCOL_VERSION);
+    if(flags & SPAMC_CHECK_ONLY) 
+      len=snprintf(buf, bufsiz, "CHECK %s\r\n", PROTOCOL_VERSION);
+    else if(flags & SPAMC_REPORT_IFSPAM)
+      len=snprintf(buf, bufsiz, "REPORT_IFSPAM %s\r\n", PROTOCOL_VERSION);
+    else if(flags & SPAMC_REPORT) 
+      len=snprintf(buf, bufsiz, "REPORT %s\r\n", PROTOCOL_VERSION);
+    else if(flags & SPAMC_SYMBOLS) 
+      len=snprintf(buf, bufsiz, "SYMBOLS %s\r\n", PROTOCOL_VERSION);
+    else
+      len=snprintf(buf, bufsiz, "PROCESS %s\r\n", PROTOCOL_VERSION);
+
     if(len<0 || len >= bufsiz){ free(m->out); m->out=m->msg; m->out_len=m->msg_len; return EX_OSERR; }
     if(username!=NULL){
         len+=i=snprintf(buf+len, bufsiz-len, "User: %s\r\n", username);
@@ -527,8 +1012,12 @@ static int _message_filter(const struct sockaddr *addr,
 
     libspamc_timeout = m->timeout;
 
-    if((i=try_to_connect(addr, (struct hostent *) hent,
-                       hent_port, &sock)) != EX_OK)
+    if ( tp->socketpath )
+        rc = try_to_connect_unix(tp, &sock);
+    else
+        rc = try_to_connect_tcp(tp, &sock);
+
+    if ( rc != EX_OK )
     {
         free(m->out); m->out=m->msg; m->out_len=m->msg_len;
         return i;
@@ -549,131 +1038,91 @@ static int _message_filter(const struct sockaddr *addr,
       SSL_write(ssl, m->msg, m->msg_len);
 #endif
     } else {
-      full_write(sock, (unsigned char *) buf, len);
-      full_write(sock, (unsigned char *) m->msg, m->msg_len);
+      full_write(sock, buf, len);
+      full_write(sock, m->msg, m->msg_len);
       shutdown(sock, SHUT_WR);
     }
 
-    /* Now, read from spamd */
-    for(len=0; len<bufsiz; len++) {
-       if(flags&SPAMC_USE_SSL) {
-#ifdef SPAMC_SSL
-         i=timeout_read(SSL_read, ssl, buf+len, 1);
-#endif
-       } else {
-         i=timeout_read(read, sock, buf+len, 1);
-       }
-
-        if(i<0){
-           failureval = EX_IOERR; goto failure;
-        }
-        if(i==0){
-            /* Read to end of message! Must be a version <1.0 server */
-            if(len<100){
-                /* Nope, communication error */
-               failureval = EX_IOERR; goto failure;
-            }
-            break;
-        }
-        if(buf[len]=='\n'){
-            buf[len]='\0';
-            if(sscanf(buf, "SPAMD/%f %d %*s", &version, &response)!=2){
-                syslog(LOG_ERR, "spamd responded with bad string '%s'", buf);
-               failureval = EX_PROTOCOL; goto failure;
-            }
-            header_read=-1;
-            break;
-        }
-    }
-    if(!header_read){
-        /* No header, so it must be a version <1.0 server */
-        memcpy(m->out, buf, len);
-        m->out_len=len;
-    } else {
-        /* Handle different versioned headers */
-        if(version-1.0>0.01){
-            for(len=0; len<bufsiz; len++){
-#ifdef SPAMC_SSL
-             if(flags&SPAMC_USE_SSL){
-               i=timeout_read(SSL_read, ssl, buf+len, 1);
-             } else{
-#endif
-               i=timeout_read(read, sock, buf+len, 1);
-#ifdef SPAMC_SSL
-             }
-#endif
-                if(i<=0){
-                   failureval = (i<0)?EX_IOERR:EX_PROTOCOL; goto failure;
-                }
-                if(buf[len]=='\n'){
-                    buf[len]='\0';
-                    if(flags&SPAMC_CHECK_ONLY){
-                        /* Check only mode, better be "Spam: x; y / x" */
-                        i=sscanf(buf, "Spam: %5s ; %f / %f", is_spam, &m->score, &m->threshold);
-                        
-                        if(i!=3){
-                            free(m->out); m->out=m->msg; m->out_len=m->msg_len;
-                            return EX_PROTOCOL;
-                        }
-                        m->out_len=snprintf(m->out, m->max_len+EXPANSION_ALLOWANCE, "%.1f/%.1f\n", m->score, m->threshold);
-                        m->is_spam=strcasecmp("true", is_spam)?EX_NOTSPAM:EX_ISSPAM;
-                        close(sock);
-                        return EX_OK;
-                    } else {
-                        /* Not check-only, better be Content-length */
-                        if(sscanf(buf, "Content-length: %d", &expected_len)!=1){
-                           failureval = EX_PROTOCOL;
-                           goto failure;
-                        }
-                    }
-
-                    /* Should be end of headers now */
-                   if(flags&SPAMC_USE_SSL){
-#ifdef SPAMC_SSL
-                     i=timeout_read(SSL_read,ssl, buf, 2);
-#endif
-                   } else{
-                     i=full_read (sock, (unsigned char *) buf, 2, 2);
-                   }
-
-                    if(i!=2 || buf[0]!='\r' || buf[1]!='\n'){
-                        /* Nope, bail. */
-                       failureval = EX_PROTOCOL; goto failure;
-                    }
+    /* ok, now read and parse it.  SPAMD/1.2 line first... */
+    failureval = _spamc_read_full_line (m, flags, ssl, sock, buf, &len, bufsiz);
+    if (failureval != EX_OK) { goto failure; }
 
-                    break;
-                }
-            }
-        }
+    if(sscanf(buf, "SPAMD/%18s %d %*s", versbuf, &response)!=2) {
+       syslog(LOG_ERR, "spamd responded with bad string '%s'", buf);
+       failureval = EX_PROTOCOL; goto failure;
     }
 
-    if(flags&SPAMC_CHECK_ONLY){
-        /* We should have gotten headers back... Damnit. */
+    versbuf[19] = '\0';
+    version = _locale_safe_string_to_float (versbuf, 20);
+    if (version < 1.0) {
+       syslog(LOG_ERR, "spamd responded with bad version string '%s'", versbuf);
        failureval = EX_PROTOCOL; goto failure;
     }
 
-    if(flags&SPAMC_USE_SSL){
-#ifdef SPAMC_SSL
-      len=timeout_read(SSL_read,ssl, m->out+m->out_len,
-                m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
-#endif
-    } else{
-      len=full_read(sock, (unsigned char *) m->out+m->out_len,
-                m->max_len+EXPANSION_ALLOWANCE+1-m->out_len,
-                m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
+    m->score = 0;
+    m->threshold = 0;
+    m->is_spam = EX_TOOBIG;
+    while (1) {
+       failureval = _spamc_read_full_line (m, flags, ssl, sock, buf, &len, bufsiz);
+       if (failureval != EX_OK) { goto failure; }
+
+       if (len == 0 && buf[0] == '\0') {
+           break;      /* end of headers */
+       }
+
+       if (_handle_spamd_header(m, flags, buf, len) < 0) {
+           failureval = EX_PROTOCOL; goto failure;
+       }
     }
 
-    if(len+m->out_len>m->max_len+EXPANSION_ALLOWANCE){
-       failureval = EX_TOOBIG; goto failure;
+    len = 0;           /* overwrite those headers */
+
+    if (flags&SPAMC_CHECK_ONLY) {
+       close(sock); sock = -1;
+       if (m->is_spam == EX_TOOBIG) {
+             /* We should have gotten headers back... Damnit. */
+             failureval = EX_PROTOCOL; goto failure;
+       }
+       return EX_OK;
     }
-    m->out_len+=len;
+    else {
+       if (m->content_length < 0) {
+           /* should have got a length too. */
+           failureval = EX_PROTOCOL; goto failure;
+       }
 
-    shutdown(sock, SHUT_RD);
-    close(sock);
+       /* have we already got something in the buffer (e.g. REPORT and
+         * REPORT_IFSPAM both create a line from the "Spam:" hdr)?  If
+        * so, add the size of that so our sanity check passes.
+        */
+       if (m->out_len > 0) {
+           m->content_length += m->out_len;
+       }
+
+       if (flags&SPAMC_USE_SSL) {
+         len = full_read_ssl (ssl, (unsigned char *) m->out+m->out_len,
+                    m->max_len+EXPANSION_ALLOWANCE+1-m->out_len,
+                    m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
+       } else{
+         len = full_read (sock, m->out+m->out_len,
+                    m->max_len+EXPANSION_ALLOWANCE+1-m->out_len,
+                    m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
+       }
+
+
+       if(len+m->out_len>m->max_len+EXPANSION_ALLOWANCE){
+           failureval = EX_TOOBIG; goto failure;
+       }
+       m->out_len+=len;
+
+       shutdown(sock, SHUT_RD);
+       close(sock); sock = -1;
+    }
     libspamc_timeout = 0;
 
-    if(m->out_len!=expected_len){
-        syslog(LOG_ERR, "failed sanity check, %d bytes claimed, %d bytes seen", expected_len, m->out_len);
+    if(m->out_len!=m->content_length) {
+        syslog(LOG_ERR, "failed sanity check, %d bytes claimed, %d bytes seen",
+                               m->content_length, m->out_len);
        failureval = EX_PROTOCOL; goto failure;
     }
 
@@ -681,61 +1130,31 @@ static int _message_filter(const struct sockaddr *addr,
 
 failure:
     free(m->out); m->out=m->msg; m->out_len=m->msg_len;
-    close(sock);
+    if (sock != -1) {
+      close(sock);
+    }
     libspamc_timeout = 0;
 
+    if(flags&SPAMC_USE_SSL) {
 #ifdef SPAMC_SSL
-    if(flags&SPAMC_USE_SSL){
       SSL_free(ssl);
       SSL_CTX_free(ctx);
-    }
 #endif
+    }
     return failureval;
 }
 
-static int _lookup_host(const char *hostname, struct hostent *out_hent)
-{
-    struct hostent *hent = NULL;
-    int origherr;
-
-    /* no need to try using inet_addr(), gethostbyname() will do that */
-
-    if (NULL == (hent = gethostbyname(hostname))) {
-        origherr = h_errno;    /* take a copy before syslog() */
-        syslog (LOG_ERR, "gethostbyname(%s) failed: h_errno=%d",
-                hostname, origherr);
-        switch(origherr)
-        {
-        case HOST_NOT_FOUND:
-        case NO_ADDRESS:
-        case NO_RECOVERY:
-                  return EX_NOHOST;
-        case TRY_AGAIN:
-                  return EX_TEMPFAIL;
-                default:
-                  return EX_OSERR;
-        }
-    }
-
-    memcpy (out_hent, hent, sizeof(struct hostent));
 
-    return EX_OK;
-}
-
-int message_process(const char *hostname, int port, char *username, int max_size, int in_fd, int out_fd, const int flags){
-    struct hostent hent;
+int message_process(struct transport *trans, char *username, int max_size, int in_fd, int out_fd, const int flags){
     int ret;
     struct message m;
 
     m.type=MESSAGE_NONE;
 
-    ret=lookup_host_for_failover(hostname, &hent);
-    if(ret!=EX_OK) goto FAIL;
-    
     m.max_len=max_size;
     ret=message_read(in_fd, flags, &m);
     if(ret!=EX_OK) goto FAIL;
-    ret=message_filter_with_failover(&hent, port, username, flags, &m);
+    ret=message_filter(trans, username, flags, &m);
     if(ret!=EX_OK) goto FAIL;
     if(message_write(out_fd, &m)<0) goto FAIL;
     if(m.is_spam!=EX_TOOBIG) {
@@ -747,7 +1166,7 @@ int message_process(const char *hostname, int port, char *username, int max_size
 
 FAIL:
    if(flags&SPAMC_CHECK_ONLY){
-       full_write(out_fd, (unsigned char *) "0/0\n", 4);
+       full_write(out_fd, "0/0\n", 4);
        message_cleanup(&m);
        return EX_NOTSPAM;
    } else {
@@ -760,46 +1179,246 @@ FAIL:
 void message_cleanup(struct message *m) {
    if (m->out != NULL && m->out != m->raw) free(m->out);
    if (m->raw != NULL) free(m->raw);
+   if (m->priv != NULL) free(m->priv);
    clear_message(m);
 }
 
 /* Aug 14, 2002 bj: Obsolete! */
-int process_message(const char *hostname, int port, char *username, int max_size, int in_fd, int out_fd, const int my_check_only, const int my_safe_fallback){
+int process_message(struct transport *tp, char *username, int max_size, int in_fd, int out_fd, const int my_check_only, const int my_safe_fallback){
     int flags;
 
     flags=SPAMC_RAW_MODE;
     if(my_check_only) flags|=SPAMC_CHECK_ONLY;
     if(my_safe_fallback) flags|=SPAMC_SAFE_FALLBACK;
 
-    return message_process(hostname, port, username, max_size, in_fd, out_fd, flags);
+    return message_process(tp, username, max_size, in_fd, out_fd, flags);
 }
 
-/* public APIs, which call into the static code and enforce sockaddr-OR-hostent
- * conventions */
+/*
+ * init_transport()
+ *
+ *     Given a pointer to a transport structure, set it to "all empty".
+ *     The default is a localhost connection.
+ */
+void transport_init(struct transport *tp)
+{
+       assert(tp != 0);
 
-int lookup_host(const char *hostname, int port, struct sockaddr *out_addr)
+       memset(tp, 0, sizeof *tp);
+
+       tp->type = TRANSPORT_LOCALHOST;
+       tp->port = 783;
+}
+
+/*
+ * randomize_hosts()
+ *
+ *     Given the transport object that contains one or more IP addresses
+ *     in this "hosts" list, rotate it by a random number of shifts to
+ *     randomize them - this is a kind of load balancing. It's possible
+ *     that the random number will be 0, which says not to touch. We don't
+ *     do anything unless 
+ */
+
+static void randomize_hosts(struct transport *tp)
 {
-  struct sockaddr_in *addr = (struct sockaddr_in *)out_addr;
-  struct hostent hent;
-  int ret;
-
-  memset(&out_addr, 0, sizeof(out_addr));
-  addr->sin_family=AF_INET;
-  addr->sin_port=htons(port);
-  ret = _lookup_host(hostname, &hent);
-  memcpy (&(addr->sin_addr), hent.h_addr, sizeof(addr->sin_addr));
-  return ret;
+int     rnum;
+
+       assert(tp != 0);
+
+       if ( tp->nhosts <= 1 ) return;
+
+       rnum = rand() % tp->nhosts;
+
+       while ( rnum-- > 0 )
+       {
+       struct in_addr  tmp = tp->hosts[0];
+       int             i;
+
+               for (i = 1; i < tp->nhosts; i++ )
+                       tp->hosts[i-1] = tp->hosts[i];
+
+               tp->hosts[i-1] = tmp;
+       }
+}
+
+/*
+ * transport_setup()
+ *
+ *     Given a "transport" object that says how we're to connect to the
+ *     spam daemon, perform all the initial setup required to make the
+ *     connection process a smooth one. The main work is to do the host
+ *     name lookup and copy over all the IP addresses to make a local copy
+ *     so they're not kept in the resolver's static state.
+ *
+ *     Here we also manage quasi-load balancing and failover: if we're
+ *     doing load balancing, we randomly "rotate" the list to put it in
+ *     a different order, and then if we're not doing failover we limit
+ *     the hosts to just one. This way *all* connections are done with
+ *     the intention of failover - makes the code a bit more clear.
+ */
+int transport_setup(struct transport *tp, int flags)
+{
+struct hostent *hp = 0;
+char           **addrp;
+
+       assert(tp != 0);
+
+       switch ( tp->type )
+       {
+         case TRANSPORT_UNIX:
+               assert(tp->socketpath != 0);
+               return EX_OK;
+
+         case TRANSPORT_LOCALHOST:
+               tp->hosts[0].s_addr = inet_addr("127.0.0.1");
+               tp->nhosts          = 1;
+               return EX_OK;
+
+         case TRANSPORT_TCP:
+               if (NULL == (hp = gethostbyname(tp->hostname)))
+               {
+               int     origherr = h_errno;  /* take a copy before syslog() */
+
+                       syslog (LOG_ERR, "gethostbyname(%s) failed: h_errno=%d",
+                               tp->hostname, origherr);
+                       switch (origherr)
+                       {
+                         case HOST_NOT_FOUND:
+                         case NO_ADDRESS:
+                         case NO_RECOVERY:
+                               return EX_NOHOST;
+                         case TRY_AGAIN:
+                               return EX_TEMPFAIL;
+                         default:
+                               return EX_OSERR;
+                       }
+               }
+
+               /*--------------------------------------------------------
+                * If we have no hosts at all, or if they are some other
+                * kind of address family besides IPv4, then we really
+                * just have no hosts at all.
+                */
+               if ( hp->h_addr_list[0] == 0 )
+               {
+                       /* no hosts in this list */
+                       return EX_NOHOST;
+               }
+
+               if ( hp->h_length   != sizeof tp->hosts[0]
+                 || hp->h_addrtype != AF_INET )
+               {
+                       /* FAIL - bad size/protocol/family? */
+                       return EX_NOHOST;
+               }
+
+               /*--------------------------------------------------------
+                * Copy all the IP addresses into our private structure.
+                * This gets them out of the resolver's static area and
+                * means we won't ever walk all over the list with other
+                * calls.
+                */
+               tp->nhosts = 0;
+
+               for (addrp = hp->h_addr_list; *addrp; addrp++)
+               {
+                       if (tp->nhosts >= TRANSPORT_MAX_HOSTS-1) {
+                               syslog (LOG_ERR, "hit limit of %d hosts, ignoring remainder", TRANSPORT_MAX_HOSTS-1);
+                               break;
+                       }
+
+                       memcpy(&tp->hosts[tp->nhosts], *addrp,
+                               sizeof tp->hosts[0]);
+
+                       tp->nhosts++;
+               }
+
+               /*--------------------------------------------------------
+                * QUASI-LOAD-BALANCING
+                *
+                * If the user wants to do quasi load balancing, "rotate"
+                * the list by a random amount based on the current time.
+                * This may later be truncated to a single item. This is
+                * meaningful only if we have more than one host.
+                */
+               if ( (flags & SPAMC_RANDOMIZE_HOSTS)  &&  tp->nhosts > 1 )
+               {
+                       randomize_hosts(tp);
+               }
+
+               /*--------------------------------------------------------
+                * If the user wants no fallback, simply truncate the host
+                * list to just one - this pretends that this is the extent
+                * of our connection list - then it's not a special case.
+                */
+               if ( !(flags & SPAMC_SAFE_FALLBACK)  &&  tp->nhosts > 1 )
+               {
+                       /* truncating list */
+                       tp->nhosts = 1;
+               }
+       }
+       return EX_OK;
 }
 
-int lookup_host_for_failover(const char *hostname, struct hostent *hent) {
-  return _lookup_host(hostname, hent);
+
+/* --------------------------------------------------------------------------- */
+
+/*
+ * Unit tests.  Must be built externally, e.g.:
+ *
+ * gcc -g -DLIBSPAMC_UNIT_TESTS spamd/spamc.c spamd/libspamc.c spamd/utils.c -o libspamctest
+ * ./libspamctest
+ *
+ */
+#ifdef LIBSPAMC_UNIT_TESTS
+
+static void
+_test_locale_safe_string_to_float_val (float input) {
+  char inputstr[99], cmpbuf1[99], cmpbuf2[99];
+  float output;
+
+  snprintf (inputstr, 99, "%f", input);
+  output = _locale_safe_string_to_float (inputstr, 99);
+  if (input == output) { return; }
+
+  /* could be a rounding error.  print as string and compare those */
+  snprintf (cmpbuf1, 98, "%f", input);
+  snprintf (cmpbuf2, 98, "%f", output);
+  if (!strcmp (cmpbuf1, cmpbuf2)) { return; }
+
+  printf ("FAIL: input=%f != output=%f\n", input, output);
 }
 
-int message_filter(const struct sockaddr *addr, char *username, int flags,
-                struct message *m)
-{ return _message_filter (addr, NULL, 0, username, flags, m); }
+static void
+unit_test_locale_safe_string_to_float (void) {
+  float statictestset[] = {    /* will try both +ve and -ve */
+       0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001,
+       9.1, 9.91, 9.991, 9.9991, 9.99991, 9.999991,
+       0.0             /* end of set constant */
+  };
+  float num;
+  int i;
+
+  printf ("starting unit_test_locale_safe_string_to_float\n");
+  /* tests of precision */
+  for (i = 0; statictestset[i] != 0.0; i++) {
+    _test_locale_safe_string_to_float_val (statictestset[i]);
+    _test_locale_safe_string_to_float_val (-statictestset[i]);
+    _test_locale_safe_string_to_float_val (1-statictestset[i]);
+    _test_locale_safe_string_to_float_val (1+statictestset[i]);
+  }
+  /* now exhaustive, in steps of 0.01 */
+  for (num = -1000.0; num < 1000.0; num += 0.01) {
+    _test_locale_safe_string_to_float_val (num);
+  }
+  printf ("finished unit_test_locale_safe_string_to_float\n");
+}
 
-int message_filter_with_failover (const struct hostent *hent, int port,
-                char *username, int flags, struct message *m)
-{ return _message_filter (NULL, hent, port, username, flags, m); }
+void
+do_libspamc_unit_tests (void) {
+  unit_test_locale_safe_string_to_float();
+  exit (0);
+}
 
+#endif /* LIBSPAMC_UNIT_TESTS */