/* <@LICENSE>
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at:
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </@LICENSE>
 */

/* 
  Compile with extra warnings -- gcc only, not suitable for use as default:

  gcc -Wextra -Wdeclaration-after-statement -Wall -g -O2 spamc/spamc.c \
  spamc/getopt.c spamc/libspamc.c spamc/utils.c -o spamc/spamc -ldl -lz
 */

#include "config.h"
#include "claws-features.h"
#include "libspamc.h"

#include <stdarg.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#define snprintf _snprintf
#define vsnprintf _vsnprintf
#define strcasecmp stricmp
#define sleep Sleep
#include <io.h>
#else
#include <strings.h>
#include <syslog.h>
#include <unistd.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>
#define closesocket(x) close(x)
#endif

#ifdef HAVE_SYSEXITS_H
#include <sysexits.h>
#endif
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef HAVE_SYS_ERRNO_H
#include <sys/errno.h>
#endif
#ifdef HAVE_TIME_H
#include <time.h>
#endif
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#ifdef HAVE_ZLIB_H
#include <zlib.h>
#endif

/* must load *after* errno.h, Bug 6697 */
#include "file-utils.h"
#include "utils.h"

/* RedHat 5.2 doesn't define Shutdown 2nd Parameter Constants */
/* KAM 12-4-01 */
/* 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
#define h_errno errno
#endif

#ifdef _WIN32
#define spamc_get_errno()   WSAGetLastError()
#else
#define spamc_get_errno()   errno
#endif

#ifndef HAVE_OPTARG
extern char *optarg;
#endif

#ifndef HAVE_INADDR_NONE
#define INADDR_NONE             ((in_addr_t) 0xffffffff)
#endif

/* jm: turned off for now, it should not be necessary. */
#undef USE_TCP_NODELAY

#ifndef HAVE_EX__MAX
/* jm: very conservative figure, should be well out of range on almost all NIXes */
#define EX__MAX 200
#endif

#undef DO_CONNECT_DEBUG_SYSLOGS
/*
#define DO_CONNECT_DEBUG_SYSLOGS 1
#define CONNECT_DEBUG_LEVEL LOG_DEBUG
*/

/* bug 4477 comment 14 */
#ifdef NI_MAXHOST
#define SPAMC_MAXHOST NI_MAXHOST
#else
#define SPAMC_MAXHOST 256
#endif

#ifdef NI_MAXSERV
#define SPAMC_MAXSERV NI_MAXSERV
#else
#define SPAMC_MAXSERV 256
#endif

/* static const int ESC_PASSTHROUGHRAW = EX__MAX + 666;  No longer seems to be used */

/* set EXPANSION_ALLOWANCE to something more than might be
   added to a message in X-headers and the report template */
static const int EXPANSION_ALLOWANCE = 16384;

/* set NUM_CHECK_BYTES to number of bytes that have to match at beginning and end
   of the data streams before and after processing by spamd 
   Aug  7 2002 jm: no longer seems to be used
   static const int NUM_CHECK_BYTES = 32;
 */

/* Set the protocol version that this spamc speaks */
static const char *PROTOCOL_VERSION = "SPAMC/1.5";

/* "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 alloced_size;           /* allocated space for the "out" buffer */

    void (*spamc_header_callback)(struct message *m, int flags, char *buf, int len);
    void (*spamd_header_callback)(struct message *m, int flags, const char *buf, int len);
};

void (*libspamc_log_callback)(int flags, int level, char *msg, va_list args) = NULL;

int libspamc_timeout = 0;
int libspamc_connect_timeout = 0;	/* Sep 8, 2008 mrgus: separate connect timeout */

/*
 * 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 family (PF_INET or PF_INET6 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.
 */
#ifdef SPAMC_HAS_ADDRINFO
static int _opensocket(int flags, struct addrinfo *res, int *psock)
{
#else
static int _opensocket(int flags, int type, int *psock)
{
    int proto = 0;
#endif
    const char *typename;
    int origerr;
#ifdef _WIN32
    int socktout;
#endif

    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.
	 */
#ifdef SPAMC_HAS_ADDRINFO
    switch(res->ai_family) {
       case PF_UNIX:
          typename = "PF_UNIX";
          break;
       case PF_INET:
          typename = "PF_INET";
          break;
       case PF_INET6:
          typename = "PF_INET6";
          break;
       default:
          typename = "Unknown";
          break;
    }
#else
    if (type == PF_UNIX) {
	typename = "PF_UNIX";
    }
    else {
	typename = "PF_INET";
	proto = IPPROTO_TCP;
    }
#endif

#ifdef DO_CONNECT_DEBUG_SYSLOGS
    libspamc_log(flags, CONNECT_DEBUG_LEVEL, "dbg: create socket(%s)", typename);
#endif

#ifdef SPAMC_HAS_ADDRINFO
    if ((*psock = socket(res->ai_family, res->ai_socktype, res->ai_protocol))
#else
    if ((*psock = socket(type, SOCK_STREAM, proto))
#endif
#ifndef _WIN32
	< 0
#else
	== INVALID_SOCKET
#endif
	) {

		/*--------------------------------------------------------
		 * 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 = spamc_get_errno();
#ifndef _WIN32
	libspamc_log(flags, LOG_ERR, "socket(%s) to spamd failed: %s", typename, strerror(origerr));
#else
	libspamc_log(flags, LOG_ERR, "socket(%s) to spamd failed: %d", typename, origerr);
#endif

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

#ifdef _WIN32
    /* bug 4344: makes timeout functional on Win32 */
    socktout = libspamc_timeout * 1000;
    if (type == PF_INET
        && setsockopt(*psock, SOL_SOCKET, SO_RCVTIMEO, (char *)&socktout, sizeof(socktout)) != 0)
    {

        origerr = spamc_get_errno();
        switch (origerr)
        {
        case EBADF:
        case ENOTSOCK:
        case ENOPROTOOPT:
        case EFAULT:
            libspamc_log(flags, LOG_ERR, "setsockopt(SO_RCVTIMEO) failed: %d", origerr);
            closesocket(*psock);
            return EX_SOFTWARE;

        default:
            break;		/* ignored */
        }
    }
#endif

	/*----------------------------------------------------------------
	 * 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
#ifdef PF_INET6
              || type == PF_INET6
#endif
             ) && setsockopt(*psock, 0, TCP_NODELAY, &one, sizeof one) != 0) {
	    origerr = spamc_get_errno();
	    switch (origerr) {
	    case EBADF:
	    case ENOTSOCK:
	    case ENOPROTOOPT:
	    case EFAULT:
		libspamc_log(flags, LOG_ERR,
#ifndef _WIN32
		       "setsockopt(TCP_NODELAY) failed: %s", strerror(origerr));
#else
		       "setsockopt(TCP_NODELAY) failed: %d", origerr);
#endif
		closesocket(*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)
{
#ifndef _WIN32
    int mysock, status, origerr;
    struct sockaddr_un addrbuf;
#ifdef SPAMC_HAS_ADDRINFO
    struct addrinfo hints, *res;
#else
    int res = PF_UNIX;
#endif
    int ret;

    assert(tp != 0);
    assert(sockptr != 0);
    assert(tp->socketpath != 0);

#ifdef SPAMC_HAS_ADDRINFO
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = PF_UNIX;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = 0;
    res = &hints;
#endif
	/*----------------------------------------------------------------
	 * If the socket itself can't be created, this is a fatal error.
	 */
    if ((ret = _opensocket(tp->flags, res, &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
    libspamc_log(tp->flags, CONNECT_DEBUG_LEVEL, "dbg: connect(AF_UNIX) to spamd at %s",
	   addrbuf.sun_path);
#endif

    status = timeout_connect(mysock, (struct sockaddr *) &addrbuf, sizeof(addrbuf));

    origerr = errno;

    if (status >= 0) {
#ifdef DO_CONNECT_DEBUG_SYSLOGS
	libspamc_log(tp->flags, CONNECT_DEBUG_LEVEL, "dbg: connect(AF_UNIX) ok");
#endif

	*sockptr = mysock;

	return EX_OK;
    }

    libspamc_log(tp->flags, LOG_ERR, "connect(AF_UNIX) to spamd using --socket='%s' failed: %s",
	   addrbuf.sun_path, strerror(origerr));
    closesocket(mysock);

    return _translate_connect_errno(origerr);
#else
    (void) tp; /* not used. suppress compiler warning */
    (void) sockptr; /* not used. suppress compiler warning */
    return EX_OSERR;
#endif
}

/*
 * 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;
#ifdef SPAMC_HAS_ADDRINFO
    struct addrinfo *res = NULL;
    char port[SPAMC_MAXSERV-1]; /* port, for logging */
#else
    int res = PF_INET;
#endif
    char host[SPAMC_MAXHOST-1]; /* hostname, for logging */
    int connect_retries, retry_sleep;

    assert(tp != 0);
    assert(sockptr != 0);
    assert(tp->nhosts > 0);

    /* default values */
    retry_sleep = tp->retry_sleep;
    connect_retries = tp->connect_retries;
    if (connect_retries == 0) {
      connect_retries = 3;
    }
    if (retry_sleep < 0) {
      retry_sleep = 1;
    }

    for (numloops = 0; numloops < connect_retries; numloops++) {
        const int hostix = numloops % tp->nhosts;
        int status, mysock;
        int innocent = 0;

                /*--------------------------------------------------------
                * We always start by creating the socket, as we get only
                * one attempt to connect() on each one. If this fails,
                * we're done.
                */

#ifdef SPAMC_HAS_ADDRINFO
        res = tp->hosts[hostix];
        while(res) {
#ifdef DO_CONNECT_DEBUG_SYSLOGS
            char *family = NULL;
            switch(res->ai_family) {
            case AF_INET:
                family = "AF_INET";
                break;
            case AF_INET6:
                family = "AF_INET6";
                break;
            default:
                family = "Unknown";
                break;
            }
#endif

            if ((ret = _opensocket(tp->flags, res, &mysock)) != EX_OK) {
                res = res->ai_next;
                continue;
            }

            getnameinfo(res->ai_addr, res->ai_addrlen,
                  host, sizeof(host),
                  port, sizeof(port),
                  NI_NUMERICHOST|NI_NUMERICSERV);

#ifdef DO_CONNECT_DEBUG_SYSLOGS
            libspamc_log(tp->flags, CONNECT_DEBUG_LEVEL,
              "dbg: connect(%s) to spamd (host %s, port %s) (try #%d of %d)",
                      family, host, port, numloops + 1, connect_retries);
#endif

            /* this is special-cased so that we have an address we can
             * safely use as an "always fail" test case */
            if (!strcmp(host, "255.255.255.255")) {
              libspamc_log(tp->flags, LOG_ERR,
                          "connect to spamd on %s failed, broadcast addr",
                          host);
              status = -1;
            }
            else {
              status = timeout_connect(mysock, res->ai_addr, res->ai_addrlen);
              if (status != 0) origerr = spamc_get_errno();
            }

#else
	    struct sockaddr_in addrbuf;
	    const char *ipaddr;
	    const char* family="AF_INET";
	    if ((ret = _opensocket(tp->flags, 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);

            /* make a copy in host, for logging (bug 5577) */
            strncpy (host, ipaddr, sizeof(host) - 1);

#ifdef DO_CONNECT_DEBUG_SYSLOGS
	    libspamc_log(tp->flags, LOG_DEBUG,
			 "dbg: connect(AF_INET) to spamd at %s (try #%d of %d)",
			 ipaddr, numloops + 1, connect_retries);
#endif

            /* this is special-cased so that we have an address we can
             * safely use as an "always fail" test case */
            if (!strcmp(ipaddr, "255.255.255.255")) {
              libspamc_log(tp->flags, LOG_ERR,
                          "connect to spamd on %s failed, broadcast addr",
                          ipaddr);
              status = -1;
            }
            else {
              status = timeout_connect(mysock, (struct sockaddr *) &addrbuf,
                        sizeof(addrbuf));
              if (status != 0) origerr = spamc_get_errno();
            }

#endif

            if (status != 0) {
                  closesocket(mysock);

                  innocent = origerr == ECONNREFUSED && numloops+1 < tp->nhosts;
                  libspamc_log(tp->flags, innocent ? LOG_DEBUG : LOG_ERR,
                      "connect to spamd on %s failed, retrying (#%d of %d): %s",
                      host, numloops+1, connect_retries,
#ifdef _WIN32
                      origerr
#else
                      strerror(origerr)
#endif
                  );

            } else {
#ifdef DO_CONNECT_DEBUG_SYSLOGS
                  libspamc_log(tp->flags, CONNECT_DEBUG_LEVEL,
                          "dbg: connect(%s) to spamd done",family);
#endif
                  *sockptr = mysock;

                  return EX_OK;
            }
#ifdef SPAMC_HAS_ADDRINFO
            res = res->ai_next;
        }
#endif
        if (numloops+1 < connect_retries && !innocent) sleep(retry_sleep);
    } /* for(numloops...) */

    libspamc_log(tp->flags, LOG_ERR,
              "connection attempt to spamd aborted after %d retries",
              connect_retries);

    return _translate_connect_errno(origerr);
}

#ifdef SPAMC_SSL
static char * _ssl_err_as_string (void) {
    BIO *bio = BIO_new(BIO_s_mem());
    ERR_print_errors(bio);
    char *buf = NULL;
    size_t len = BIO_get_mem_data(bio, &buf);
    char *ret = (char *)calloc(1, 1 + len);
    if (!ret) {
        BIO_free(bio);
        char *err = "(could not get SSL error)";
        return err;
    }
    memcpy(ret, buf, len);
    BIO_free(bio);
    /* Only return up to first newline */
    char *lf = strchr(ret, '\n');
    if (lf)
        *lf = '\0';
    return ret;
}

static SSL_CTX * _try_ssl_ctx_init(int flags)
{
    const SSL_METHOD *meth;
    SSL_CTX *ctx;

    SSLeay_add_ssl_algorithms();
    SSL_load_error_strings();
    /* this method allows negotiation of version */
    meth = SSLv23_client_method();
    ctx = SSL_CTX_new(meth);
    if (ctx == NULL) {
        libspamc_log(flags, LOG_ERR, "cannot create SSL CTX context: %s",
                     _ssl_err_as_string());
        return NULL;
    }
    if (flags & SPAMC_TLSV1) {
	/* allow TLSv1.0 or better */
	SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3);
    } else {
	/* allow SSLv3 or better */
	SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2);
    }
    SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
    return ctx;
}

static int _try_ssl_connect(SSL_CTX *ctx, struct transport *tp,
			    SSL **pssl, int flags, int sock)
{
    SSL *ssl;
    int ssl_rtn;
    if (tp->ssl_ca_file || tp->ssl_ca_path) {
	if (!SSL_CTX_load_verify_locations(ctx, tp->ssl_ca_file,
					   tp->ssl_ca_path)) {
	    libspamc_log(flags, LOG_ERR,
			 "error loading CA file %s or path %s: %s",
			 tp->ssl_ca_file ? tp->ssl_ca_file : "(void)",
			 tp->ssl_ca_path ? tp->ssl_ca_path : "(void)",
			 _ssl_err_as_string());
	    return EX_OSERR;
	}
	SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
    } else {
        SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
    }
    if (flags & SPAMC_CLIENT_SSL_CERT) {
	/* libspamc_log(flags, LOG_ERR, "loading client cert %s key %s",
		     tp->ssl_cert_file, tp->ssl_key_file); */
	if (!SSL_CTX_use_certificate_file(ctx, tp->ssl_cert_file,
					  SSL_FILETYPE_PEM)) {
	    libspamc_log(flags, LOG_ERR,
			 "unable to load certificate file %s: %s",
			 tp->ssl_cert_file, _ssl_err_as_string());
	    return EX_OSERR;
	}
	if (!SSL_CTX_use_PrivateKey_file(ctx, tp->ssl_key_file,
					 SSL_FILETYPE_PEM)) {
	    libspamc_log(flags, LOG_ERR,
			 "unable to load key file %s: %s",
			 tp->ssl_key_file, _ssl_err_as_string());
	    return EX_OSERR;
	}
	if (!SSL_CTX_check_private_key(ctx)) {
	    libspamc_log(flags, LOG_ERR,
			 "key file %s and cert file %s do not match: %s",
			 tp->ssl_key_file, tp->ssl_cert_file,
			 _ssl_err_as_string());
	    return EX_OSERR;
	}
    }
    ssl = SSL_new(ctx);
    if (ssl == NULL) {
        libspamc_log(flags, LOG_ERR,
	             "SSL_new failed: %s", _ssl_err_as_string());
        return EX_OSERR;
    }
    *pssl = ssl;
    if (!SSL_set_fd(ssl, sock)) {
	libspamc_log(flags, LOG_ERR,
		     "SSL_set_fd failed: %s", _ssl_err_as_string());
	return EX_OSERR;
    }
    ssl_rtn = SSL_connect(ssl);
    if (ssl_rtn != 1) {
	int ssl_err = SSL_get_error(ssl, ssl_rtn);
	libspamc_log(flags, LOG_ERR,
		     "SSL_connect error: %s", _ssl_err_as_string());
	return EX_UNAVAILABLE;
    }
    return EX_OK;
}
#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
 * of helper functions.
 */

static void _clear_message(struct message *m)
{
    m->type = MESSAGE_NONE;
    m->raw = NULL;
    m->raw_len = 0;
    m->pre = NULL;
    m->pre_len = 0;
    m->msg = NULL;
    m->msg_len = 0;
    m->post = NULL;
    m->post_len = 0;
    m->is_spam = EX_TOOBIG;
    m->score = 0.0;
    m->threshold = 0.0;
    m->outbuf = NULL;
    m->out = NULL;
    m->out_len = 0;
    m->content_length = -1;
}

static void _free_zlib_buffer(unsigned char **zlib_buf, int *zlib_bufsiz)
{
	if(*zlib_buf) {
	free(*zlib_buf);
	*zlib_buf=NULL;
	}
	*zlib_bufsiz=0;
}

static void _use_msg_for_out(struct message *m)
{
    if (m->outbuf)
	free(m->outbuf);
    m->outbuf = NULL;
    m->out = m->msg;
    m->out_len = m->msg_len;
}

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, 1, 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;
    }
    m->type = MESSAGE_ERROR;
    if (m->raw_len > (int) m->max_len)
    {
        libspamc_log(m->priv->flags, LOG_NOTICE,
                "skipped message, greater than max message size (%d bytes)",
                m->max_len);
	return EX_TOOBIG;
    }
    m->type = MESSAGE_RAW;
    m->msg = m->raw;
    m->msg_len = m->raw_len;
    m->out = m->msg;
    m->out_len = m->msg_len;
    return EX_OK;
}

static int _message_read_bsmtp(int fd, struct message *m)
{
    unsigned int i, j, p_len;
    char prev;
    char* p;

    _clear_message(m);
    if ((m->raw = malloc(m->max_len + 1)) == NULL)
	return EX_OSERR;

    /* Find the DATA line */
    m->raw_len = full_read(fd, 1, 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;
    }
    m->type = MESSAGE_ERROR;
    if (m->raw_len > (int) m->max_len)
	return EX_TOOBIG;
    p = m->pre = m->raw;
    /* Search for \nDATA\n which marks start of actual message */
    while ((p_len = (m->raw_len - (p - m->raw))) > 8) { /* leave room for at least \nDATA\n.\n */
      char* q = memchr(p, '\n', p_len - 8);  /* find next \n then see if start of \nDATA\n */
      if (q == NULL) break;
      q++;
      if (((q[0]|0x20) == 'd') && /* case-insensitive ASCII comparison */
	  ((q[1]|0x20) == 'a') &&
	  ((q[2]|0x20) == 't') &&
	  ((q[3]|0x20) == 'a')) {
	q+=4;
	if (q[0] == '\r') ++q;
	if (*(q++) == '\n') {  /* leave q at start of message if we found it */
	  m->msg = q;
	  m->pre_len = q - m->raw;
	  m->msg_len = m->raw_len - m->pre_len;
	  break;
	}
      }
      p = q; /* the above code ensures no other '\n' comes before q */
    }
    if (m->msg == NULL)
	return EX_DATAERR;

    /* ensure this is >= 0 */
    if (m->msg_len < 0) {
	return EX_SOFTWARE;
    }

    /* Find the end-of-DATA line */
    prev = '\n';
    for (i = j = 0; i < (unsigned int) m->msg_len; i++) {
	if (prev == '\n' && m->msg[i] == '.') {
	    /* Dot at the beginning of a line */
            if (((int) (i+1) == m->msg_len)
                || ((int) (i+1) < m->msg_len && m->msg[i + 1] == '\n')
                || ((int) (i+2) < m->msg_len && m->msg[i + 1] == '\r' && m->msg[i + 2] == '\n')) {
		/* Lone dot! That's all, folks */
		m->post = m->msg + i;
		m->post_len = m->msg_len - i;
		m->msg_len = j;
		break;
	    }
	    else if ((int) (i+1) < m->msg_len && m->msg[i + 1] == '.') {
		/* Escaping dot, eliminate. */
		prev = '.';
		continue;
	    }			/* Else an ordinary dot, drop down to ordinary char handler */
	}
	prev = m->msg[i];
	m->msg[j++] = m->msg[i];
    }

    /* if bad format with no end "\n.\n", error out */
    if (m->post == NULL)
	return EX_DATAERR;
    m->type = MESSAGE_BSMTP;
    m->out = m->msg;
    m->out_len = m->msg_len;
    return EX_OK;
}

int message_read(int fd, int flags, struct message *m)
{
    assert(m != NULL);

    libspamc_timeout = 0;

    /* create the "private" part of the struct message */
    m->priv = malloc(sizeof(struct libspamc_private_message));
    if (m->priv == NULL) {
	libspamc_log(flags, LOG_ERR, "message_read: malloc failed");
	return EX_OSERR;
    }
    m->priv->flags = flags;
    m->priv->alloced_size = 0;
    m->priv->spamc_header_callback = 0;
    m->priv->spamd_header_callback = 0;

    if (flags & SPAMC_PING) {
      _clear_message(m);
      return EX_OK;
    }

    switch (flags & SPAMC_MODE_MASK) {
    case SPAMC_RAW_MODE:
	return _message_read_raw(fd, m);

    case SPAMC_BSMTP_MODE:
	return _message_read_bsmtp(fd, m);

    default:
	libspamc_log(flags, LOG_ERR, "message_read: Unknown mode %d",
		flags & SPAMC_MODE_MASK);
	return EX_USAGE;
    }
}

long message_write(int fd, struct message *m)
{
    long total = 0;
    off_t i, j;
    off_t jlimit;
    char buffer[1024];

    assert(m != NULL);

    if (m->priv->flags & (SPAMC_CHECK_ONLY|SPAMC_PING)) {
	if (m->is_spam == EX_ISSPAM || m->is_spam == EX_NOTSPAM) {
	    return full_write(fd, 1, m->out, m->out_len);

	}
	else {
	    libspamc_log(m->priv->flags, LOG_ERR, "oops! SPAMC_CHECK_ONLY is_spam: %d",
                        m->is_spam);
	    return -1;
	}
    }

    /* else we're not in CHECK_ONLY mode */
    switch (m->type) {
    case MESSAGE_NONE:
	libspamc_log(m->priv->flags, LOG_ERR, "Cannot write this message, it's MESSAGE_NONE!");
	return -1;

    case MESSAGE_ERROR:
	return full_write(fd, 1, m->raw, m->raw_len);

    case MESSAGE_RAW:
	return full_write(fd, 1, m->out, m->out_len);

    case MESSAGE_BSMTP:
	total = full_write(fd, 1, 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 && j < jlimit;) {
		if (i + 1 < m->out_len && m->out[i] == '\n'
		    && m->out[i + 1] == '.') {
		    if (j > jlimit - 4) {
			break;	/* avoid overflow */
		    }
		    buffer[j++] = m->out[i++];
		    buffer[j++] = m->out[i++];
		    buffer[j++] = '.';
		}
		else {
		    buffer[j++] = m->out[i++];
		}
	    }
	    total += full_write(fd, 1, buffer, j);
	}
	return total + full_write(fd, 1, m->post, m->post_len);

    default:
	libspamc_log(m->priv->flags, LOG_ERR, "Unknown message type %d", m->type);
	return -1;
    }
}

void message_dump(int in_fd, int out_fd, struct message *m, int flags)
{
    char buf[8196];
    int bytes;

    if (m == NULL) {
	libspamc_log(flags, LOG_ERR, "oops! message_dump called with NULL message");
	return;
    }

    if (m->type != MESSAGE_NONE) {
	message_write(out_fd, m);
    }

    while ((bytes = full_read(in_fd, 1, buf, 8192, 8192)) > 0) {
	if (bytes != full_write(out_fd, 1, buf, bytes)) {
	    libspamc_log(flags, LOG_ERR, "oops! message_dump of %d returned different",
		   bytes);
	}
    }
}

static int
_spamc_read_full_line(struct message *m, int flags, SSL * ssl, int sock,
		      char *buf, size_t *lenp, size_t bufsiz)
{
    int failureval;
    int bytesread = 0;
    size_t len;

    UNUSED_VARIABLE(m);

    *lenp = 0;
    /* 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, 0, buf + len, 1);
	}

	if (bytesread <= 0) {
	    failureval = EX_IOERR;
	    goto failure;
	}

	if (buf[len] == '\n') {
	    buf[len] = '\0';
	    if (len > 0 && buf[len - 1] == '\r') {
		len--;
		buf[len] = '\0';
	    }
	    *lenp = len;
	    return EX_OK;
	}
    }

    libspamc_log(flags, 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));
    /* note: don't compare floats == 0.0, it's unsafe.  use a range */
    if (postdot >= -0.00001 && postdot <= 0.00001) {
	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,
		     unsigned int *didtellflags)
{
    char is_spam[6];
    char s_str[21], t_str[21];
    char didset_ret[15];
    char didremove_ret[15];

    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);

	/* set bounds on these to ensure no buffer overflow in the sprintf */
	if (m->score > 1e10)
	    m->score = 1e10;
	else if (m->score < -1e10)
	    m->score = -1e10;
	if (m->threshold > 1e10)
	    m->threshold = 1e10;
	else if (m->threshold < -1e10)
	    m->threshold = -1e10;

	/* 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 = sprintf(m->out,
				 "%.1f/%.1f\n", m->score, m->threshold);
	}
	else if ((flags & SPAMC_REPORT_IFSPAM && m->is_spam == EX_ISSPAM)
		 || (flags & SPAMC_REPORT)) {
	    m->out_len = sprintf(m->out,
				 "%.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) {
	    libspamc_log(flags, LOG_ERR, "spamd responded with bad Content-length '%s'",
		   buf);
	    return EX_PROTOCOL;
	}
	return EX_OK;
    }
    else if (sscanf(buf, "DidSet: %14s", didset_ret) == 1) {
      if (strstr(didset_ret, "local")) {
	  *didtellflags |= SPAMC_SET_LOCAL;
	}
	if (strstr(didset_ret, "remote")) {
	  *didtellflags |= SPAMC_SET_REMOTE;
	}
    }
    else if (sscanf(buf, "DidRemove: %14s", didremove_ret) == 1) {
        if (strstr(didremove_ret, "local")) {
	  *didtellflags |= SPAMC_REMOVE_LOCAL;
	}
	if (strstr(didremove_ret, "remote")) {
	  *didtellflags |= SPAMC_REMOVE_REMOTE;
	}
    }
    else if (m->priv->spamd_header_callback != NULL)
      m->priv->spamd_header_callback(m, flags, buf, len);

    return EX_OK;
}

static int
_zlib_compress (char *m_msg, int m_msg_len,
        unsigned char **zlib_buf, int *zlib_bufsiz, int flags)
{
    int rc;
    int len, totallen;

#ifndef HAVE_LIBZ

    UNUSED_VARIABLE(m_msg);
    UNUSED_VARIABLE(m_msg_len);
    UNUSED_VARIABLE(zlib_buf);
    UNUSED_VARIABLE(zlib_bufsiz);
    UNUSED_VARIABLE(rc);
    UNUSED_VARIABLE(len);
    UNUSED_VARIABLE(totallen);
    libspamc_log(flags, LOG_ERR, "spamc not built with zlib support");
    return EX_SOFTWARE;

#else
    z_stream strm;

    UNUSED_VARIABLE(flags);

    /* worst-case, according to http://www.zlib.org/zlib_tech.html ;
      * same as input, plus 5 bytes per 16k, plus 6 bytes.  this should
      * be plenty */
    *zlib_bufsiz = (int) (m_msg_len * 1.0005) + 1024;
    *zlib_buf = (unsigned char *) malloc (*zlib_bufsiz);
    if (*zlib_buf == NULL) {
        return EX_OSERR;
    }

    strm.zalloc = Z_NULL;
    strm.zfree = Z_NULL;
    strm.opaque = Z_NULL;
    rc = deflateInit(&strm, 3);
    if (rc != Z_OK) {
        return EX_OSERR;
    }

    strm.avail_in = m_msg_len;
    strm.next_in = (unsigned char *) m_msg;
    strm.avail_out = *zlib_bufsiz;
    strm.next_out = (unsigned char *) *zlib_buf;

    totallen = 0;
    do {
        rc = deflate(&strm, Z_FINISH);
        assert(rc != Z_STREAM_ERROR);
        len = (size_t) (*zlib_bufsiz - strm.avail_out);
        strm.next_out += len;
        totallen += len;
    } while (strm.avail_out == 0);

    *zlib_bufsiz = totallen;
    return EX_OK;

#endif
}

int
_append_original_body (struct message *m, int flags)
{
    char *cp, *cpend, *bodystart;
    int bodylen, outspaceleft, towrite;

    /* at this stage, m->out now contains the rewritten headers.
     * find and append the raw message's body, up to m->priv->alloced_size
     * bytes.
     */

#define CRNLCRNL        "\r\n\r\n"
#define CRNLCRNL_LEN    4
#define NLNL            "\n\n"
#define NLNL_LEN        2

    cpend = m->raw + m->raw_len;
    bodystart = NULL;

    for (cp = m->raw; cp < cpend; cp++) {
        if (*cp == '\r' && cpend - cp >= CRNLCRNL_LEN && 
                            !strncmp(cp, CRNLCRNL, CRNLCRNL_LEN))
        {
            bodystart = cp + CRNLCRNL_LEN;
            break;
        }
        else if (*cp == '\n' && cpend - cp >= NLNL_LEN && 
                           !strncmp(cp, NLNL, NLNL_LEN))
        {
            bodystart = cp + NLNL_LEN;
            break;
        }
    }

    if (bodystart == NULL) {
        libspamc_log(flags, LOG_ERR, "failed to find end-of-headers");
        return EX_SOFTWARE;
    }

    bodylen = cpend - bodystart;
    outspaceleft = (m->priv->alloced_size-1) - m->out_len;
    towrite = (bodylen < outspaceleft ? bodylen : outspaceleft);

    /* copy in the body; careful not to overflow */
    strncpy (m->out + m->out_len, bodystart, towrite);
    m->out_len += towrite;
    return EX_OK;
}

int message_filter(struct transport *tp, const char *username,
                   int flags, struct message *m)
{
    char buf[8192];
    size_t bufsiz = (sizeof(buf) / sizeof(*buf)) - 4; /* bit of breathing room */
    size_t len;
    int sock = -1;
    int rc;
    char versbuf[20];
    float version;
    int response;
    int failureval = EX_SOFTWARE;
    unsigned int throwaway;
    SSL_CTX *ctx = NULL;
    SSL *ssl = NULL;
    char zlib_on = 0;
    unsigned char *zlib_buf = NULL;
    int zlib_bufsiz = 0;
    unsigned char *towrite_buf;
    int towrite_len;
    int filter_retry_count;
    int filter_retry_sleep;
    int filter_retries;
    #ifdef SPAMC_HAS_ADDRINFO
        struct addrinfo *tmphost;
    #else
        struct in_addr tmphost;
    #endif
    int nhost_counter;

    assert(tp != NULL);
    assert(m != NULL);

    if ((flags & SPAMC_USE_ZLIB) != 0) {
      zlib_on = 1;
    }

    if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
        ctx = _try_ssl_ctx_init(flags);
        if (ctx == NULL) {
	    failureval = EX_OSERR;
	    goto failure;
        }
#else
	UNUSED_VARIABLE(ssl);
	UNUSED_VARIABLE(ctx);
	libspamc_log(flags, LOG_ERR, "spamc not built with SSL support");
	return EX_SOFTWARE;
#endif
    }

    m->is_spam = EX_TOOBIG;

    if (m->outbuf != NULL)
        free(m->outbuf);
    m->priv->alloced_size = m->max_len + EXPANSION_ALLOWANCE + 1;
    if ((m->outbuf = malloc(m->priv->alloced_size)) == NULL) {
	failureval = EX_OSERR;
	goto failure;
    }
    m->out = m->outbuf;
    m->out_len = 0;

    /* If the spamd filter takes too long and we timeout, then
     * retry again.  This gets us around a hung child thread 
     * in spamd or a problem on a spamd host in a multi-host
     * setup.  If there is more than one destination host
     * we move to the next host on each attempt.
     */

    /* default values */
    filter_retry_sleep = tp->filter_retry_sleep;
    filter_retries = tp->filter_retries;
    if (filter_retries == 0) {
        filter_retries = 1;
    }
    if (filter_retry_sleep < 0) {
        filter_retry_sleep = 1;
    }

    /* filterloop - Ensure that we run through this at least
     * once, and again if there are errors 
     */
    filter_retry_count = 0;
    while ((filter_retry_count==0) || 
                ((filter_retry_count<tp->filter_retries) && (failureval == EX_IOERR)))
    {
        if (filter_retry_count != 0){
            /* Ensure that the old socket gets closed */
            if (sock != -1) {
                closesocket(sock);
                sock=-1;
            }

            /* Move to the next host in the list, if nhosts>1 */
            if (tp->nhosts > 1) {
                tmphost = tp->hosts[0];

                /* TODO: free using freeaddrinfo() */
                for (nhost_counter = 1; nhost_counter < tp->nhosts; nhost_counter++) {
                    tp->hosts[nhost_counter - 1] = tp->hosts[nhost_counter];
                }
        
                tp->hosts[nhost_counter - 1] = tmphost;
            }

            /* Now sleep the requested amount */
            sleep(filter_retry_sleep);
        }

        filter_retry_count++;
    
        /* Build spamd protocol header */
        if (flags & SPAMC_CHECK_ONLY)
          strcpy(buf, "CHECK ");
        else if (flags & SPAMC_REPORT_IFSPAM)
          strcpy(buf, "REPORT_IFSPAM ");
        else if (flags & SPAMC_REPORT)
          strcpy(buf, "REPORT ");
        else if (flags & SPAMC_SYMBOLS)
          strcpy(buf, "SYMBOLS ");
        else if (flags & SPAMC_PING)
          strcpy(buf, "PING ");
        else if (flags & SPAMC_HEADERS)
          strcpy(buf, "HEADERS ");
        else
          strcpy(buf, "PROCESS ");
    
        len = strlen(buf);
        if (len + strlen(PROTOCOL_VERSION) + 2 >= bufsiz) {
            _use_msg_for_out(m);
            return EX_OSERR;
        }
    
        strcat(buf, PROTOCOL_VERSION);
        strcat(buf, "\r\n");
        len = strlen(buf);
    
        towrite_buf = (unsigned char *) m->msg;
        towrite_len = (int) m->msg_len;
        if (zlib_on) {
            if (_zlib_compress(m->msg, m->msg_len, &zlib_buf, &zlib_bufsiz, flags) != EX_OK)
            {
                _free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
                return EX_OSERR;
            }
            towrite_buf = zlib_buf;
            towrite_len = zlib_bufsiz;
        }
    
        if (!(flags & SPAMC_PING)) {
          if (username != NULL) {
              if (strlen(username) + 8 >= (bufsiz - len)) {
                  _use_msg_for_out(m);
                  if (zlib_on) {
                      _free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
                  }
                  return EX_OSERR;
              }
              strcpy(buf + len, "User: ");
              strcat(buf + len, username);
              strcat(buf + len, "\r\n");
              len += strlen(buf + len);
          }
          if (zlib_on) {
              len += snprintf(buf + len, 8192-len, "Compress: zlib\r\n");
          }
          if ((m->msg_len > SPAMC_MAX_MESSAGE_LEN) || ((len + 27) >= (bufsiz - len))) {
              _use_msg_for_out(m);
              if (zlib_on) {
                  _free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
              }
              return EX_DATAERR;
          }
          len += snprintf(buf + len, 8192-len, "Content-length: %d\r\n", (int) towrite_len);
        }
        /* bug 6187, PING needs empty line too, bumps protocol version to 1.5 */
        len += snprintf(buf + len, 8192-len, "\r\n");
    
        libspamc_timeout = m->timeout;
        libspamc_connect_timeout = m->connect_timeout;	/* Sep 8, 2008 mrgus: separate connect timeout */

        if (tp->socketpath)
          rc = _try_to_connect_unix(tp, &sock);
        else
          rc = _try_to_connect_tcp(tp, &sock);
    
        if (rc != EX_OK) {
          _use_msg_for_out(m);
          if (zlib_on) {
              _free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
          }
          return rc;      /* use the error code try_to_connect_*() gave us. */
        }
    
        if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
            rc = _try_ssl_connect(ctx, tp, &ssl, flags, sock);
            if (rc != EX_OK) {
                failureval = rc;
                goto failure;
            }
#endif
        }
    
        /* Send to spamd */
        if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
            rc = SSL_write(ssl, buf, len);
            if (rc <= 0) {
                libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
                             SSL_get_error(ssl, rc));
                failureval = EX_IOERR;
                goto failure;
            }
            rc = SSL_write(ssl, towrite_buf, towrite_len);
            if (rc <= 0) {
                libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
                             SSL_get_error(ssl, rc));
                failureval = EX_IOERR;
                goto failure;
            }
            SSL_shutdown(ssl);
            shutdown(sock, SHUT_WR);
#endif
        }
        else {
            full_write(sock, 0, buf, len);
            full_write(sock, 0, towrite_buf, towrite_len);
            shutdown(sock, SHUT_WR);
        }

        /* free zlib buffer
        * bug 6025: zlib buffer not freed if compression is used
        */
        if (zlib_on) {
            _free_zlib_buffer(&zlib_buf, &zlib_bufsiz);
        }
    
        /* ok, now read and parse it.  SPAMD/1.2 line first... */
        failureval =
                _spamc_read_full_line(m, flags, ssl, sock, buf, &len, bufsiz);
    } /* end of filterloop */

    if (failureval != EX_OK) {
        goto failure;
    }

    if (sscanf(buf, "SPAMD/%18s %d %*s", versbuf, &response) != 2) {
	libspamc_log(flags, LOG_ERR, "spamd responded with bad string '%s'", buf);
	failureval = EX_PROTOCOL;
	goto failure;
    }

    versbuf[19] = '\0';
    version = _locale_safe_string_to_float(versbuf, 20);
    if (version < 1.0) {
	libspamc_log(flags, LOG_ERR, "spamd responded with bad version string '%s'",
	       versbuf);
	failureval = EX_PROTOCOL;
	goto failure;
    }

    if (flags & SPAMC_PING) {
	closesocket(sock);
	sock = -1;
        m->out_len = sprintf(m->out, "SPAMD/%s %d\n", versbuf, response);
        m->is_spam = EX_NOTSPAM;
        return EX_OK;
    }

    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, &throwaway) < 0) {
	    failureval = EX_PROTOCOL;
	    goto failure;
	}
    }

    len = 0;			/* overwrite those headers */

    if (flags & SPAMC_CHECK_ONLY) {
	closesocket(sock);
	sock = -1;
	if (m->is_spam == EX_TOOBIG) {
	    /* We should have gotten headers back... Damnit. */
	    failureval = EX_PROTOCOL;
	    goto failure;
	}
	return EX_OK;
    }
    else {
	if (m->content_length < 0) {
	    /* should have got a length too. */
	    failureval = EX_PROTOCOL;
	    goto failure;
	}

	/* 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->priv->alloced_size - m->out_len,
				m->priv->alloced_size - m->out_len);
	}
	else {
	    len = full_read(sock, 0, m->out + m->out_len,
			    m->priv->alloced_size - m->out_len,
			    m->priv->alloced_size - m->out_len);
	}


	if ((int) len + (int) m->out_len > (m->priv->alloced_size - 1)) {
	    failureval = EX_TOOBIG;
	    goto failure;
	}
	m->out_len += len;

	shutdown(sock, SHUT_RD);
	closesocket(sock);
	sock = -1;
    }
    libspamc_timeout = 0;

    if (m->out_len != m->content_length) {
	libspamc_log(flags, LOG_ERR,
	       "failed sanity check, %d bytes claimed, %d bytes seen",
	       m->content_length, m->out_len);
	failureval = EX_PROTOCOL;
	goto failure;
    }

    if (flags & SPAMC_HEADERS) {
        if (_append_original_body(m, flags) != EX_OK) {
            goto failure;
        }
    }

    return EX_OK;

  failure:
	_use_msg_for_out(m);
    if (sock != -1) {
	closesocket(sock);
    }
    libspamc_timeout = 0;

    if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
	SSL_free(ssl);
	SSL_CTX_free(ctx);
#endif
    }
    return failureval;
}

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;

    assert(trans != NULL);

    m.type = MESSAGE_NONE;

    /* enforce max_size being unsigned, therefore >= 0 */
    if (max_size < 0) {
	ret = EX_SOFTWARE;
        goto FAIL;
    }
    m.max_len = (unsigned int) max_size;

    ret = message_read(in_fd, flags, &m);
    if (ret != EX_OK)
        goto FAIL;
    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) {
        message_cleanup(&m);
        return m.is_spam;
    }
    message_cleanup(&m);
    return ret;

  FAIL:
    if (flags & SPAMC_CHECK_ONLY) {
        full_write(out_fd, 1, "0/0\n", 4);
        message_cleanup(&m);
        return EX_NOTSPAM;
    }
    else {
        message_dump(in_fd, out_fd, &m, flags);
        message_cleanup(&m);
        return ret;
    }
}

int message_tell(struct transport *tp, const char *username, int flags,
		 struct message *m, int msg_class,
		 unsigned int tellflags, unsigned int *didtellflags)
{
    char buf[8192];
    size_t bufsiz = (sizeof(buf) / sizeof(*buf)) - 4; /* bit of breathing room */
    size_t len;
    int sock = -1;
    int rc;
    char versbuf[20];
    float version;
    int response;
    int failureval;
    SSL_CTX *ctx = NULL;
    SSL *ssl = NULL;

    assert(tp != NULL);
    assert(m != NULL);

    if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
        ctx = _try_ssl_ctx_init(flags);
        if (ctx == NULL) {
            failureval = EX_OSERR;
            goto failure;
        }
#else
	UNUSED_VARIABLE(ssl);
	UNUSED_VARIABLE(ctx);
	libspamc_log(flags, LOG_ERR, "spamc not built with SSL support");
	return EX_SOFTWARE;
#endif
    }

    m->is_spam = EX_TOOBIG;

    if (m->outbuf != NULL)
        free(m->outbuf);
    m->priv->alloced_size = m->max_len + EXPANSION_ALLOWANCE + 1;
    if ((m->outbuf = malloc(m->priv->alloced_size)) == NULL) {
	failureval = EX_OSERR;
	goto failure;
    }
    m->out = m->outbuf;
    m->out_len = 0;

    /* Build spamd protocol header */
    strcpy(buf, "TELL ");

    len = strlen(buf);
    if (len + strlen(PROTOCOL_VERSION) + 2 >= bufsiz) {
	_use_msg_for_out(m);
	return EX_OSERR;
    }

    strcat(buf, PROTOCOL_VERSION);
    strcat(buf, "\r\n");
    len = strlen(buf);

    if (msg_class != 0) {
      strcpy(buf + len, "Message-class: ");
      if (msg_class == SPAMC_MESSAGE_CLASS_SPAM) {
	strcat(buf + len, "spam\r\n");
      }
      else {
	strcat(buf + len, "ham\r\n");
      }
      len += strlen(buf + len);
    }

    if ((tellflags & SPAMC_SET_LOCAL) || (tellflags & SPAMC_SET_REMOTE)) {
      int needs_comma_p = 0;
      strcat(buf + len, "Set: ");
      if (tellflags & SPAMC_SET_LOCAL) {
	strcat(buf + len, "local");
	needs_comma_p = 1;
      }
      if (tellflags & SPAMC_SET_REMOTE) {
	if (needs_comma_p == 1) {
	  strcat(buf + len, ",");
	}
	strcat(buf + len, "remote");
      }
      strcat(buf + len, "\r\n");
      len += strlen(buf + len);
    }

    if ((tellflags & SPAMC_REMOVE_LOCAL) || (tellflags & SPAMC_REMOVE_REMOTE)) {
      int needs_comma_p = 0;
      strcat(buf + len, "Remove: ");
      if (tellflags & SPAMC_REMOVE_LOCAL) {
	strcat(buf + len, "local");
	needs_comma_p = 1;
      }
      if (tellflags & SPAMC_REMOVE_REMOTE) {
	if (needs_comma_p == 1) {
	  strcat(buf + len, ",");
	}
	strcat(buf + len, "remote");
      }
      strcat(buf + len, "\r\n");
      len += strlen(buf + len);
    }

    if (username != NULL) {
	if (strlen(username) + 8 >= (bufsiz - len)) {
	    _use_msg_for_out(m);
	    return EX_OSERR;
	}
	strcpy(buf + len, "User: ");
	strcat(buf + len, username);
	strcat(buf + len, "\r\n");
	len += strlen(buf + len);
    }
    if ((m->msg_len > SPAMC_MAX_MESSAGE_LEN) || ((len + 27) >= (bufsiz - len))) {
	_use_msg_for_out(m);
	return EX_DATAERR;
    }
    len += sprintf(buf + len, "Content-length: %d\r\n\r\n", (int) m->msg_len);

    if (m->priv->spamc_header_callback != NULL) {
      char buf2[1024];
      m->priv->spamc_header_callback(m, flags, buf2, 1024);
      strncat(buf, buf2, bufsiz - len);
    }

    libspamc_timeout = m->timeout;
    libspamc_connect_timeout = m->connect_timeout;	/* Sep 8, 2008 mrgus: separate connect timeout */

    if (tp->socketpath)
	rc = _try_to_connect_unix(tp, &sock);
    else
	rc = _try_to_connect_tcp(tp, &sock);

    if (rc != EX_OK) {
	_use_msg_for_out(m);
	return rc;      /* use the error code try_to_connect_*() gave us. */
    }

    if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
        rc = _try_ssl_connect(ctx, tp, &ssl, flags, sock);
        if (rc != EX_OK) {
            failureval = rc;
            goto failure;
        }
#endif
    }

    /* Send to spamd */
    if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
	rc = SSL_write(ssl, buf, len);
        if (rc <= 0) {
            libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
                         SSL_get_error(ssl, rc));
            failureval = EX_IOERR;
            goto failure;
        }
	rc = SSL_write(ssl, m->msg, m->msg_len);
        if (rc <= 0) {
            libspamc_log(flags, LOG_ERR, "SSL write failed (%d)",
                         SSL_get_error(ssl, rc));
            failureval = EX_IOERR;
            goto failure;
        }
	SSL_shutdown(ssl);
	shutdown(sock, SHUT_WR);
#endif
    }
    else {
	full_write(sock, 0, buf, len);
	full_write(sock, 0, m->msg, m->msg_len);
	shutdown(sock, SHUT_WR);
    }

    /* 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;
    }

    if (sscanf(buf, "SPAMD/%18s %d %*s", versbuf, &response) != 2) {
	libspamc_log(flags, LOG_ERR, "spamd responded with bad string '%s'", buf);
	failureval = EX_PROTOCOL;
	goto failure;
    }

    versbuf[19] = '\0';
    version = _locale_safe_string_to_float(versbuf, 20);
    if (version < 1.0) {
	libspamc_log(flags, LOG_ERR, "spamd responded with bad version string '%s'",
	       versbuf);
	failureval = EX_PROTOCOL;
	goto failure;
    }

    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, didtellflags) < 0) {
	    failureval = EX_PROTOCOL;
	    goto failure;
	}
    }

    len = 0;			/* overwrite those headers */

    shutdown(sock, SHUT_RD);
    closesocket(sock);
    sock = -1;

    libspamc_timeout = 0;

    return EX_OK;

  failure:
    _use_msg_for_out(m);
    if (sock != -1) {
        closesocket(sock);
    }
    libspamc_timeout = 0;

    if (flags & SPAMC_USE_SSL) {
#ifdef SPAMC_SSL
	SSL_free(ssl);
	SSL_CTX_free(ctx);
#endif
    }
    return failureval;
}

void message_cleanup(struct message *m)
{
    assert(m != NULL);
    if (m->outbuf != NULL)
        free(m->outbuf);
    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(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(tp, username, max_size, in_fd, out_fd, flags);
}

/*
* 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);

    memset(tp, 0, sizeof *tp);

    tp->type = TRANSPORT_LOCALHOST;
    tp->port = 783;
    tp->flags = 0;
    tp->retry_sleep = -1;
}

/*
* 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)
{
#ifdef SPAMC_HAS_ADDRINFO
    struct addrinfo *tmp;
#else
    struct in_addr tmp;
#endif
    int i;
    int rnum;

    assert(tp != 0);

    if (tp->nhosts <= 1)
        return;

    rnum = rand() % tp->nhosts;

    while (rnum-- > 0) {
        tmp = tp->hosts[0];

        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)
{
#ifdef SPAMC_HAS_ADDRINFO
    struct addrinfo hints, *res; /*, *addrp; */
    char port[6];
    int origerr;
#else
    struct hostent *hp;
    char **addrp;
#endif
    char *hostlist, *hostname;
    int errbits;

#ifdef _WIN32
    /* Start Winsock up */
    WSADATA wsaData;
    int nCode;
    if ((nCode = WSAStartup(MAKEWORD(1, 1), &wsaData)) != 0) {
        printf("WSAStartup() returned error code %d\n", nCode);
        return EX_OSERR;
    }

#endif

    assert(tp != NULL);
    tp->flags = flags;

#ifdef SPAMC_HAS_ADDRINFO
    snprintf(port, 6, "%d", tp->port);

    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = 0;
    hints.ai_socktype = SOCK_STREAM;

    if (       (flags & SPAMC_USE_INET4) && !(flags & SPAMC_USE_INET6)) {
      hints.ai_family = PF_INET;
#ifdef PF_INET6
    } else if ((flags & SPAMC_USE_INET6) && !(flags & SPAMC_USE_INET4)) {
      hints.ai_family = PF_INET6;
#endif
    } else {
      hints.ai_family = PF_UNSPEC;
    }
#endif

    switch (tp->type) {
#ifndef _WIN32
    case TRANSPORT_UNIX:
        assert(tp->socketpath != 0);
        return EX_OK;
#endif
    case TRANSPORT_LOCALHOST:
#ifdef SPAMC_HAS_ADDRINFO
        /* getaddrinfo(NULL) will look up the loopback address.
         * See also bug 5057,  ::1 will be tried before 127.0.0.1
         * unless overridden (through hints) by a command line option -4
         */
        if ((origerr = getaddrinfo(NULL, port, &hints, &res)) != 0) {
            libspamc_log(flags, LOG_ERR, 
                  "getaddrinfo for a loopback address failed: %s",
                  gai_strerror(origerr));
            return EX_OSERR;
        }
        tp->hosts[0] = res;
#else
        tp->hosts[0].s_addr = inet_addr("127.0.0.1");
#endif
        tp->nhosts = 1;
        return EX_OK;

    case TRANSPORT_TCP:
        if ((hostlist = strdup(tp->hostname)) == NULL)
            return EX_OSERR;

        /* We want to return the least permanent error, in this bitmask we
         * record the errors seen with:
         *  0: no error
         *  1: EX_TEMPFAIL
         *  2: EX_NOHOST
         * EX_OSERR will return immediately.
         * Bits aren't reset so a check against nhosts is needed to determine
         * if something went wrong.
         */
        errbits = 0;
        tp->nhosts = 0;
        /* Start with char offset in front of the string because we'll add 
         * one in the loop
         */
        hostname = hostlist - 1;
        do {
            char *hostend;
            
            hostname += 1;
            hostend = strchr(hostname, ',');
            if (hostend != NULL) {
                *hostend = '\0';
            }
#ifdef SPAMC_HAS_ADDRINFO            
            if ((origerr = getaddrinfo(hostname, port, &hints, &res))) {
                libspamc_log(flags, LOG_DEBUG, 
                      "getaddrinfo(%s) failed: %s",
                      hostname, gai_strerror(origerr));
                switch (origerr) { 
                case EAI_AGAIN:
                    errbits |= 1;
                    break;
                case EAI_FAMILY: /*address family not supported*/
                case EAI_SOCKTYPE: /*socket type not supported*/
                case EAI_BADFLAGS: /*ai_flags is invalid*/
                case EAI_NONAME: /*node or service unknown*/
                case EAI_SERVICE: /*service not available*/
/* work around Cygwin IPv6 patch - err codes not defined in Windows aren't in patch */
#ifdef HAVE_EAI_ADDRFAMILY
                case EAI_ADDRFAMILY: /*no addresses in requested family*/
#endif
#ifdef HAVE_EAI_SYSTEM
                case EAI_SYSTEM: /*system error, check errno*/
#endif
#ifdef HAVE_EAI_NODATA
                case EAI_NODATA: /*address exists, but no data*/
#endif
                case EAI_MEMORY: /*out of memory*/
                case EAI_FAIL: /*name server returned permanent error*/
                    errbits |= 2;
                    break;
                default:
                    /* should not happen, all errors are checked above */
                    free(hostlist);
                    return EX_OSERR;
                }
                goto nexthost; /* try next host in list */
            }
#else
            if ((hp = gethostbyname(hostname)) == NULL) {
                int origerr = h_errno; /* take a copy before syslog() */
                libspamc_log(flags, LOG_DEBUG, "gethostbyname(%s) failed: h_errno=%d",
                    hostname, origerr);
                switch (origerr) {
                case TRY_AGAIN:
                    errbits |= 1;
                    break;
                case HOST_NOT_FOUND:
                case NO_ADDRESS:
                case NO_RECOVERY:
                    errbits |= 2;
                    break;
                default:
                    /* should not happen, all errors are checked above */
                    free(hostlist);
                    return EX_OSERR;
                }
                goto nexthost; /* try next host in list */
            }
#endif
            
            /* If we have no hosts at all */
#ifdef SPAMC_HAS_ADDRINFO
            if(res == NULL)
#else
            if (hp->h_addr_list[0] == NULL
             || hp->h_length != sizeof tp->hosts[0]
             || hp->h_addrtype != AF_INET)
                /* no hosts/bad size/wrong family */
#endif
            {
                errbits |= 1;
                goto nexthost; /* try next host in list */
            }

            /* 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.
             */
#ifdef SPAMC_HAS_ADDRINFO
            if(tp->nhosts == TRANSPORT_MAX_HOSTS) {
               libspamc_log(flags, LOG_NOTICE, 
                     "hit limit of %d hosts, ignoring remainder",
                     TRANSPORT_MAX_HOSTS);
               break;
            }

            /* treat all A or AAAA records of each host as one entry */
            tp->hosts[tp->nhosts++] = res;

            /* alternatively, treat multiple A or AAAA records
               of one host as individual entries */
/*          for (addrp = res; addrp != NULL; ) {
 *              tp->hosts[tp->nhosts] = addrp;
 *              addrp = addrp->ai_next;     /-* before NULLing ai_next *-/
 *              tp->hosts[tp->nhosts]->ai_next = NULL;
 *              tp->nhosts++;
 *          }
 */

#else
            for (addrp = hp->h_addr_list; *addrp; addrp++) {
                if (tp->nhosts == TRANSPORT_MAX_HOSTS) {
                    libspamc_log(flags, LOG_NOTICE, "hit limit of %d hosts, ignoring remainder",
                        TRANSPORT_MAX_HOSTS);
                    break;
                }
                memcpy(&tp->hosts[tp->nhosts], *addrp, hp->h_length);
                tp->nhosts++;
            }
#endif            
nexthost:
            hostname = hostend;
        } while (hostname != NULL);
        free(hostlist);
        
        if (tp->nhosts == 0) {
            if (errbits & 1) {
                libspamc_log(flags, LOG_ERR, "could not resolve any hosts (%s): a temporary error occurred",
                    tp->hostname); 
                return EX_TEMPFAIL;
            }
            else {
                libspamc_log(flags, LOG_ERR, "could not resolve any hosts (%s): no such host",
                    tp->hostname); 
                return EX_NOHOST;
            }
        }
        
        /* 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;
    }
    
    /* oops, unknown transport type */
    return EX_OSERR;
}

/*
* transport_cleanup()
*
*	Given a "transport" object that says how we're to connect to the
*	spam daemon, delete and free any buffers allocated so that it
*       can be discarded without causing a memory leak.
*/
void transport_cleanup(struct transport *tp)
{

#ifdef SPAMC_HAS_ADDRINFO
  int i;

  for(i=0;i<tp->nhosts;i++) {
      if (tp->hosts[i] != NULL) {
          freeaddrinfo(tp->hosts[i]);
          tp->hosts[i] = NULL;
      }
  }
#endif

}

/*
* register_libspamc_log_callback()
*
* Register a callback handler for libspamc_log to replace the default behaviour.
*/

void register_libspamc_log_callback(void (*function)(int flags, int level, char *msg, va_list args)) {
  libspamc_log_callback = function;
}

/*
* register_spamc_header_callback()
*
* Register a callback handler to generate spamc headers for a given message
*/

void register_spamc_header_callback(const struct message *m, void (*func)(struct message *m, int flags, char *buf, int len)) {
  m->priv->spamc_header_callback = func;
}

/*
* register_spamd_header_callback()
*
* Register a callback handler to generate spamd headers for a given message
*/

void register_spamd_header_callback(const struct message *m, void (*func)(struct message *m, int flags, const char *buf, int len)) {
  m->priv->spamd_header_callback = func;
}

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

#define LOG_BUFSIZ      1023

void
libspamc_log (int flags, int level, char *msg, ...)
{
    va_list ap;
    char buf[LOG_BUFSIZ+1];
    int len = 0;

    va_start(ap, msg);

    if ((flags & SPAMC_LOG_TO_CALLBACK) != 0 && libspamc_log_callback != NULL) {
      libspamc_log_callback(flags, level, msg, ap);
    }
    else if ((flags & SPAMC_LOG_TO_STDERR) != 0) {
        /* create a log-line buffer */
        len = snprintf(buf, LOG_BUFSIZ, "spamc: ");
        len += vsnprintf(buf+len, LOG_BUFSIZ-len, msg, ap);

        /* avoid buffer overflow */
        if (len > (LOG_BUFSIZ-2)) { len = (LOG_BUFSIZ-3); }

        len += snprintf(buf+len, LOG_BUFSIZ-len, "\n");
        buf[LOG_BUFSIZ] = '\0';     /* ensure termination */
        (void) write (2, buf, len);

    } else {
        vsnprintf(buf, LOG_BUFSIZ, msg, ap);
        buf[LOG_BUFSIZ] = '\0';     /* ensure termination */
#ifndef _WIN32
        syslog (level, "%s", buf);
#else
        (void) level;  /* not used. suppress compiler warning */
        fprintf (stderr, "%s\n", buf);
#endif
    }

    va_end(ap);
}

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

/*
* 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;

    /* sprintf instead of snprintf is safe here because it is only a controlled test */
    sprintf(inputstr, "%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 */
    sprintf(cmpbuf1, "%f", input);
    sprintf(cmpbuf2, "%f", output);
    if (!strcmp(cmpbuf1, cmpbuf2)) {
        return;
    }

    printf("FAIL: input=%f != output=%f\n", input, output);
}

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");
}

void do_libspamc_unit_tests(void)
{
    unit_test_locale_safe_string_to_float();
    exit(0);
}

#endif /* LIBSPAMC_UNIT_TESTS */
