0.9.6claws4
[claws.git] / src / plugins / spamassassin / libspamc.c
1 /*
2  * This code is copyright 2001 by Craig Hughes
3  * Portions copyright 2002 by Brad Jorsch
4  * It is licensed under the same license as Perl itself.  The text of this
5  * license is included in the SpamAssassin distribution in the file named
6  * "License".
7  */
8
9 #include "config.h"
10 #include "libspamc.h"
11 #include "utils.h"
12
13 #include <unistd.h>
14 #include <stdlib.h>
15 #include <assert.h>
16 #include <stdio.h>
17 #include <string.h>
18 #include <syslog.h>
19 #include <sys/types.h>
20 #include <sys/socket.h>
21 #include <netinet/in.h>
22 #include <sys/un.h>
23 #include <netinet/tcp.h>
24 #include <arpa/inet.h>
25
26 #ifdef HAVE_SYSEXITS_H
27 #include <sysexits.h>
28 #endif
29 #ifdef HAVE_ERRNO_H
30 #include <errno.h>
31 #endif
32 #ifdef HAVE_SYS_ERRNO_H
33 #include <sys/errno.h>
34 #endif
35 #ifdef HAVE_TIME_H
36 #include <time.h>
37 #endif
38 #ifdef HAVE_SYS_TIME_H
39 #include <sys/time.h>
40 #endif
41
42 #define MAX_CONNECT_RETRIES 3
43 #define CONNECT_RETRY_SLEEP 1
44
45 /* RedHat 5.2 doesn't define Shutdown 2nd Parameter Constants */
46 /* KAM 12-4-01 */
47 /* SJF 2003/04/25 - now test for macros directly */
48 #ifndef SHUT_RD
49 #  define SHUT_RD 0     /* no more receptions */
50 #endif
51 #ifndef SHUT_WR
52 #  define SHUT_WR 1     /* no more transmissions */
53 #endif
54 #ifndef SHUT_RDWR
55 #  define SHUT_RDWR 2   /* no more receptions or transmissions */
56 #endif
57
58 #ifndef HAVE_H_ERRNO
59 #define h_errno errno
60 #endif
61
62 #ifndef HAVE_OPTARG
63 extern char *optarg;
64 #endif
65
66 #ifndef HAVE_INADDR_NONE
67 #define INADDR_NONE             ((in_addr_t) 0xffffffff)
68 #endif
69
70 /* jm: turned off for now, it should not be necessary. */
71 #undef USE_TCP_NODELAY
72
73 #ifndef HAVE_EX__MAX
74 /* jm: very conservative figure, should be well out of range on almost all NIXes */
75 #define EX__MAX 200 
76 #endif
77
78 #undef DO_CONNECT_DEBUG_SYSLOGS
79 /* or #define DO_CONNECT_DEBUG_SYSLOGS 1 */
80
81 static const int ESC_PASSTHROUGHRAW = EX__MAX+666;
82
83 /* set EXPANSION_ALLOWANCE to something more than might be
84    added to a message in X-headers and the report template */
85 static const int EXPANSION_ALLOWANCE = 16384;
86
87 /* set NUM_CHECK_BYTES to number of bytes that have to match at beginning and end
88    of the data streams before and after processing by spamd 
89    Aug  7 2002 jm: no longer seems to be used
90    static const int NUM_CHECK_BYTES = 32;
91  */
92
93 /* Set the protocol version that this spamc speaks */
94 static const char *PROTOCOL_VERSION="SPAMC/1.3";
95
96 /* "private" part of struct message.
97  * we use this instead of the struct message directly, so that we
98  * can add new members without affecting the ABI.
99  */
100 struct libspamc_private_message {
101   int flags;    /* copied from "flags" arg to message_read() */
102 };
103
104 int libspamc_timeout = 0;
105
106 /*
107  * translate_connect_errno()
108  *
109  *      Given a UNIX error number obtained (probably) from "connect(2)",
110  *      translate this to a failure code. This module is shared by both
111  *      transport modules - UNIX and TCP.
112  *
113  *      This should ONLY be called when there is an error.
114  */
115 static int
116 translate_connect_errno(int err)
117 {
118         switch (err)
119         {
120           case EBADF:
121           case EFAULT:
122           case ENOTSOCK:
123           case EISCONN:
124           case EADDRINUSE:
125           case EINPROGRESS:
126           case EALREADY:
127           case EAFNOSUPPORT:
128                 return EX_SOFTWARE;
129
130           case ECONNREFUSED:
131           case ETIMEDOUT:
132           case ENETUNREACH:
133                 return EX_UNAVAILABLE;
134
135           case EACCES:
136                 return EX_NOPERM;
137
138           default:
139                 return EX_SOFTWARE;
140         }
141 }
142
143 /*
144  * opensocket()
145  *
146  *      Given a socket type (PF_INET or PF_UNIX), try to create this socket
147  *      and store the FD in the pointed-to place. If it's successful, do any
148  *      other setup required to make the socket ready to use, such as setting
149  *      TCP_NODELAY mode, and in any case we return EX_OK if all is well.
150  *
151  *      Upon failure we return one of the other EX_??? error codes.
152  */
153 static int opensocket(int type, int *psock)
154 {
155 const char *typename;
156 int         proto = 0;
157
158         assert(psock != 0);
159
160         /*----------------------------------------------------------------
161          * Create a few induction variables that are implied by the socket
162          * type given by the user. The typename is strictly used for debug
163          * reporting.
164          */
165         if ( type == PF_UNIX )
166         {
167                 typename = "PF_UNIX";
168         }
169         else
170         {
171                 typename = "PF_INET";
172                 proto    = IPPROTO_TCP;
173         }
174
175 #ifdef DO_CONNECT_DEBUG_SYSLOGS
176         syslog (DEBUG_LEVEL, "dbg: create socket(%s)", typename);
177 #endif
178
179         if ( (*psock = socket(type, SOCK_STREAM, proto)) < 0 )
180         {
181         int     origerr;
182
183                 /*--------------------------------------------------------
184                  * At this point we had a failure creating the socket, and
185                  * this is pretty much fatal. Translate the error reason
186                  * into something the user can understand.
187                  */
188                 origerr = errno;    /* take a copy before syslog() */
189
190                 syslog (LOG_ERR, "socket(%s) to spamd failed: %m", typename);
191
192                 switch (origerr)
193                 {
194                   case EPROTONOSUPPORT:
195                   case EINVAL:
196                         return EX_SOFTWARE;
197
198                   case EACCES:
199                         return EX_NOPERM;
200
201                   case ENFILE:
202                   case EMFILE:
203                   case ENOBUFS:
204                   case ENOMEM:
205                         return EX_OSERR;
206
207                   default:
208                         return EX_SOFTWARE;
209                 }
210         }
211
212
213         /*----------------------------------------------------------------
214          * Do a bit of setup on the TCP socket if required. Notes above
215          * suggest this is probably not set
216          */
217 #ifdef USE_TCP_NODELAY
218         {
219                 int one = 1;
220
221                 if ( type == PF_INET
222                  &&  setsockopt(*psock, 0, TCP_NODELAY, &one, sizeof one) != 0 )
223                 {
224                         switch(errno)
225                         {
226                           case EBADF:
227                           case ENOTSOCK:
228                           case ENOPROTOOPT:
229                           case EFAULT:
230                                 syslog(LOG_ERR,
231                                    "setsockopt(TCP_NODELAY) failed: %m");
232                                 close (*psock);
233                                 return EX_SOFTWARE;
234
235                           default:
236                                 break;            /* ignored */
237                         }
238                 }
239         }
240 #endif /* USE_TCP_NODELAY */
241
242         return EX_OK;   /* all is well */
243 }
244
245 /*
246  * try_to_connect_unix()
247  *
248  *      Given a transport handle that implies using a UNIX domain
249  *      socket, try to make a connection to it and store the resulting
250  *      file descriptor in *sockptr. Return is EX_OK if we did it,
251  *      and some other error code otherwise.
252  */
253 static int
254 try_to_connect_unix (struct transport *tp, int *sockptr)
255 {
256 int mysock, status, origerr;
257 struct sockaddr_un addrbuf;
258 int ret;
259
260         assert(tp             != 0);
261         assert(sockptr        != 0);
262         assert(tp->socketpath != 0);
263
264         /*----------------------------------------------------------------
265          * If the socket itself can't be created, this is a fatal error.
266          */
267         if ( (ret = opensocket(PF_UNIX, &mysock)) != EX_OK )
268                 return ret;
269
270         /* set up the UNIX domain socket */
271         memset(&addrbuf, 0, sizeof addrbuf);
272         addrbuf.sun_family = AF_UNIX;
273         strncpy(addrbuf.sun_path, tp->socketpath, sizeof addrbuf.sun_path - 1);
274         addrbuf.sun_path[sizeof addrbuf.sun_path - 1] = '\0';
275
276 #ifdef DO_CONNECT_DEBUG_SYSLOGS
277         syslog (DEBUG_LEVEL, "dbg: connect(AF_UNIX) to spamd at %s",
278                 addrbuf.sun_path);
279 #endif
280
281         status = connect(mysock, (struct sockaddr *) &addrbuf, sizeof(addrbuf));
282
283         origerr = errno;
284
285         if ( status >= 0 )
286         {
287 #ifdef DO_CONNECT_DEBUG_SYSLOGS
288                 syslog(DEBUG_LEVEL, "dbg: connect(AF_UNIX) ok");
289 #endif
290
291                 *sockptr = mysock;
292
293                 return EX_OK;
294         }
295
296         syslog(LOG_ERR, "connect(AF_UNIX) to spamd %s failed: %m",
297                 addrbuf.sun_path);
298
299         close(mysock);
300
301         return translate_connect_errno(origerr);
302 }
303
304 /*
305  * try_to_connect_tcp()
306  *
307  *      Given a transport that implies a TCP connection, either to
308  *      localhost or a list of IP addresses, attempt to connect. The
309  *      list of IP addresses has already been randomized (if requested)
310  *      and limited to just one if fallback has been enabled.
311  */
312 static int
313 try_to_connect_tcp (const struct transport *tp, int *sockptr)
314 {
315 int     numloops;
316 int     origerr = 0;
317 int     ret;
318
319         assert(tp        != 0);
320         assert(sockptr   != 0);
321         assert(tp->nhosts > 0);
322
323 #ifdef DO_CONNECT_DEBUG_SYSLOGS
324         for (numloops = 0; numloops < tp->nhosts; numloops++)
325         {
326                 syslog(LOG_ERR, "dbg: %d/%d: %s",
327                         numloops+1, tp->nhosts, inet_ntoa(tp->hosts[numloops]));
328         }
329 #endif
330
331         for (numloops = 0; numloops < MAX_CONNECT_RETRIES; numloops++)
332         {
333         struct sockaddr_in addrbuf;
334         const int          hostix = numloops % tp->nhosts;
335         int                status, mysock;
336         const char       * ipaddr;
337
338                 /*--------------------------------------------------------
339                  * We always start by creating the socket, as we get only
340                  * one attempt to connect() on each one. If this fails,
341                  * we're done.
342                  */
343                 if ( (ret = opensocket(PF_INET, &mysock)) != EX_OK )
344                         return ret;
345
346                 memset(&addrbuf, 0, sizeof(addrbuf));
347
348                 addrbuf.sin_family = AF_INET;
349                 addrbuf.sin_port   = htons(tp->port);
350                 addrbuf.sin_addr   = tp->hosts[hostix];
351
352                 ipaddr = inet_ntoa(addrbuf.sin_addr);
353
354 #ifdef DO_CONNECT_DEBUG_SYSLOGS
355                 syslog (DEBUG_LEVEL,
356                         "dbg: connect(AF_INET) to spamd at %s (try #%d of %d)",
357                         ipaddr,
358                         numloops+1,
359                         MAX_CONNECT_RETRIES);
360 #endif
361
362                 status = connect(mysock, (struct sockaddr *)&addrbuf, sizeof(addrbuf));
363
364                 if (status != 0)
365                 {
366                         syslog (LOG_ERR,
367                         "connect(AF_INET) to spamd at %s failed, retrying (#%d of %d): %m",
368                                 ipaddr, numloops+1, MAX_CONNECT_RETRIES);
369
370                         close(mysock);
371
372                         sleep(CONNECT_RETRY_SLEEP);
373                 }
374                 else
375                 {
376 #ifdef DO_CONNECT_DEBUG_SYSLOGS
377                         syslog(DEBUG_LEVEL,
378                                 "dbg: connect(AF_INET) to spamd at %s done",
379                                 ipaddr);
380 #endif
381                         *sockptr = mysock;
382
383                         return EX_OK;
384                 }
385         }
386
387         syslog (LOG_ERR, "connection attempt to spamd aborted after %d retries",
388                 MAX_CONNECT_RETRIES);
389
390         return translate_connect_errno(origerr);
391 }
392
393 #if 0
394 static int
395 try_to_connect (const struct sockaddr *argaddr, struct hostent *hent,
396                 int hent_port, int *sockptr)
397 {
398 #ifdef USE_TCP_NODELAY
399   int value;
400 #endif
401   int mysock = -1;
402   int status = -1;
403   int origerr;
404   int numloops;
405   int hostnum = 0;
406   struct sockaddr_in addrbuf, *addr;
407   struct in_addr inaddrlist[256];
408
409 #ifdef DO_CONNECT_DEBUG_SYSLOGS
410   int dbgiter; char dbgbuf[2048]; int dbgbuflen = 0;
411 #endif
412
413   /* NOTE: do not call syslog() (unless you are about to return) before
414    * we take a copy of the h_addr_list.
415    */
416
417   /* only one set of connection targets can be used.  assert this */
418   if (argaddr == NULL && hent == NULL) {
419       syslog (LOG_ERR, "oops! both NULL in try_to_connect");
420       return EX_SOFTWARE;
421   } else if (argaddr != NULL && hent != NULL) {
422       syslog (LOG_ERR, "oops! both non-NULL in try_to_connect");
423       return EX_SOFTWARE;
424   }
425
426   /* take a copy of the h_addr_list part of the struct hostent */
427   if (hent != NULL) {
428     memset (inaddrlist, 0, sizeof(inaddrlist));
429
430     for (hostnum=0; hent->h_addr_list[hostnum] != 0; hostnum++) {
431
432 #ifdef DO_CONNECT_DEBUG_SYSLOGS
433       dbgbuflen += snprintf (dbgbuf+dbgbuflen, 2047-dbgbuflen,
434                   "[%d %lx: %d.%d.%d.%d]",
435                   hostnum, hent->h_addr_list[hostnum],
436                   hent->h_addr_list[hostnum][0],
437                   hent->h_addr_list[hostnum][1],
438                   hent->h_addr_list[hostnum][2],
439                   hent->h_addr_list[hostnum][3]);
440 #endif
441
442       if (hostnum > 255) {
443         syslog (LOG_ERR, "too many address in hostent (%d), ignoring others",
444                             hostnum);
445         break;
446       }
447
448       if (hent->h_addr_list[hostnum] == NULL) {
449         /* shouldn't happen */
450         syslog (LOG_ERR, "hent->h_addr_list[hostnum] == NULL! foo!");
451         return EX_SOFTWARE;
452       }
453
454 #ifdef DO_CONNECT_DEBUG_SYSLOGS
455       dbgbuflen += snprintf (dbgbuf+dbgbuflen, 2047-dbgbuflen,
456                   "[%d: %d.%d.%d.%d] ", sizeof (struct in_addr),
457                   hent->h_addr_list[hostnum][0],
458                   hent->h_addr_list[hostnum][1],
459                   hent->h_addr_list[hostnum][2],
460                   hent->h_addr_list[hostnum][3]);
461 #endif
462
463       memcpy ((void *) &(inaddrlist[hostnum]),
464                 (void *) hent->h_addr_list[hostnum],
465                 sizeof (struct in_addr));
466     }
467
468 #ifdef DO_CONNECT_DEBUG_SYSLOGS
469       syslog (LOG_DEBUG, "dbg: %d %s", hostnum, dbgbuf); dbgbuflen = 0;
470 #endif
471   }
472
473
474 #ifdef DO_CONNECT_DEBUG_SYSLOGS
475     for (dbgiter = 0; dbgiter < hostnum; dbgiter++) {
476       syslog (LOG_DEBUG, "dbg: host addr %d/%d = %lx at %lx", dbgiter, hostnum,
477                   inaddrlist[dbgiter].s_addr, &(inaddrlist[dbgiter]));
478     }
479 #endif
480
481   hent = NULL; /* cannot use hent after this point, syslog() may overwrite it */
482
483 #ifdef DO_CONNECT_DEBUG_SYSLOGS
484   syslog (LOG_DEBUG, "dbg: socket");
485 #endif
486
487   if(-1 == (mysock = socket(PF_INET,SOCK_STREAM,0)))
488   {
489     origerr = errno;    /* take a copy before syslog() */
490     syslog (LOG_ERR, "socket() to spamd failed: %m");
491     switch(origerr)
492     {
493     case EPROTONOSUPPORT:
494     case EINVAL:
495       return EX_SOFTWARE;
496     case EACCES:
497       return EX_NOPERM;
498     case ENFILE:
499     case EMFILE:
500     case ENOBUFS:
501     case ENOMEM:
502       return EX_OSERR;
503     default:
504       return EX_SOFTWARE;
505     }
506   }
507
508 #ifdef USE_TCP_NODELAY
509   /* TODO: should this be up above the connect()? */
510   value = 1;            /* make this explicit! */
511   if(-1 == setsockopt(mysock,0,TCP_NODELAY,&value,sizeof(value)))
512   {
513     switch(errno)
514     {
515     case EBADF:
516     case ENOTSOCK:
517     case ENOPROTOOPT:
518     case EFAULT:
519       syslog (LOG_ERR, "setsockopt() to spamd failed: %m");
520       close (mysock);
521       return EX_SOFTWARE;
522
523     default:
524       break;            /* ignored */
525     }
526   }
527 #endif
528
529   for (numloops=0; numloops < MAX_CONNECT_RETRIES; numloops++) {
530   
531 #ifdef DO_CONNECT_DEBUG_SYSLOGS
532       syslog (LOG_DEBUG, "dbg: connect() to spamd %d", numloops);
533 #endif
534
535     if (argaddr != NULL) {
536       addr = (struct sockaddr_in *) argaddr;     /* use the one provided */
537   
538 #ifdef DO_CONNECT_DEBUG_SYSLOGS
539         syslog (LOG_DEBUG, "dbg: using argaddr");
540 #endif
541
542     } else {
543       /* cycle through the addrs in hent */
544       memset(&addrbuf, 0, sizeof(addrbuf));
545       addrbuf.sin_family=AF_INET;
546       addrbuf.sin_port=htons(hent_port);
547
548       if (sizeof(addrbuf.sin_addr) != sizeof(struct in_addr)) { /* shouldn't happen */
549         syslog (LOG_ERR,        
550                 "foo! sizeof(sockaddr.sin_addr) != sizeof(struct in_addr)");
551         return EX_SOFTWARE;
552       }
553
554 #ifdef DO_CONNECT_DEBUG_SYSLOGS
555         syslog (LOG_DEBUG, "dbg: cpy addr %d/%d at %lx",
556                 numloops%hostnum, hostnum, &(inaddrlist[numloops % hostnum]));
557 #endif
558
559       memcpy (&addrbuf.sin_addr, &(inaddrlist[numloops % hostnum]),
560                         sizeof(addrbuf.sin_addr));
561       addr = &addrbuf;
562
563 #ifdef DO_CONNECT_DEBUG_SYSLOGS
564         syslog (LOG_DEBUG, "dbg: conn addr %d/%d = %lx",
565             numloops%hostnum, hostnum, addrbuf.sin_addr.s_addr);
566 #endif
567
568     }
569
570 #ifdef DO_CONNECT_DEBUG_SYSLOGS
571     syslog (LOG_DEBUG, "dbg: connect() to spamd at %s",
572                 inet_ntoa(((struct sockaddr_in *)addr)->sin_addr));
573 #endif
574     status = connect(mysock,(const struct sockaddr *) addr, sizeof(*addr));
575
576 #ifdef DO_CONNECT_DEBUG_SYSLOGS
577       syslog (LOG_DEBUG, "dbg: connect() to spamd at %s done",
578           inet_ntoa(((struct sockaddr_in *)addr)->sin_addr));
579 #endif
580
581     if (status < 0)
582     {
583       origerr = errno;        /* take a copy before syslog() */
584       syslog (LOG_ERR, "connect() to spamd at %s failed, retrying (%d/%d): %m",
585                         inet_ntoa(((struct sockaddr_in *)addr)->sin_addr),
586                         numloops+1, MAX_CONNECT_RETRIES);
587       sleep(CONNECT_RETRY_SLEEP);
588
589     } else {
590       *sockptr = mysock;
591       return EX_OK;
592     }
593   }
594  
595   /* failed, even with a few retries */
596   close (mysock);
597   syslog (LOG_ERR, "connection attempt to spamd aborted after %d retries",
598        MAX_CONNECT_RETRIES);
599  
600   switch(origerr)
601   {
602    case EBADF:
603    case EFAULT:
604    case ENOTSOCK:
605    case EISCONN:
606    case EADDRINUSE:
607    case EINPROGRESS:
608    case EALREADY:
609    case EAFNOSUPPORT:
610      return EX_SOFTWARE;
611    case ECONNREFUSED:
612    case ETIMEDOUT:
613    case ENETUNREACH:
614      return EX_UNAVAILABLE;
615    case EACCES:
616      return EX_NOPERM;
617    default:
618      return EX_SOFTWARE;
619   }
620 }
621 #endif
622
623 /* Aug 14, 2002 bj: Reworked things. Now we have message_read, message_write,
624  * message_dump, lookup_host, message_filter, and message_process, and a bunch
625  * of helper functions.
626  */
627
628 static void clear_message(struct message *m){
629     m->type=MESSAGE_NONE;
630     m->raw=NULL; m->raw_len=0;
631     m->pre=NULL; m->pre_len=0;
632     m->msg=NULL; m->msg_len=0;
633     m->post=NULL; m->post_len=0;
634     m->is_spam=EX_TOOBIG;
635     m->score=0.0; m->threshold=0.0;
636     m->out=NULL; m->out_len=0;
637     m->content_length=-1;
638 }
639
640 static int
641 message_read_raw(int fd, struct message *m){
642     clear_message(m);
643     if((m->raw=malloc(m->max_len+1))==NULL) return EX_OSERR;
644     m->raw_len=full_read(fd, m->raw, m->max_len+1, m->max_len+1);
645     if(m->raw_len<=0){
646         free(m->raw); m->raw=NULL; m->raw_len=0;
647         return EX_IOERR;
648     }
649     m->type=MESSAGE_ERROR;
650     if(m->raw_len>m->max_len) return EX_TOOBIG;
651     m->type=MESSAGE_RAW;
652     m->msg=m->raw;
653     m->msg_len=m->raw_len;
654     m->out=m->msg;
655     m->out_len=m->msg_len;
656     return EX_OK;
657 }
658
659 static int message_read_bsmtp(int fd, struct message *m){
660     off_t i, j;
661     char prev;
662
663     clear_message(m);
664     if((m->raw=malloc(m->max_len+1))==NULL) return EX_OSERR;
665
666     /* Find the DATA line */
667     m->raw_len=full_read(fd, m->raw, m->max_len+1, m->max_len+1);
668     if(m->raw_len<=0){
669         free(m->raw); m->raw=NULL; m->raw_len=0;
670         return EX_IOERR;
671     }
672     m->type=MESSAGE_ERROR;
673     if(m->raw_len>m->max_len) return EX_TOOBIG;
674     m->pre=m->raw;
675     for(i=0; i<m->raw_len-6; i++){
676         if((m->raw[i]=='\n') &&
677            (m->raw[i+1]=='D' || m->raw[i+1]=='d') &&
678            (m->raw[i+2]=='A' || m->raw[i+2]=='a') &&
679            (m->raw[i+3]=='T' || m->raw[i+3]=='t') &&
680            (m->raw[i+4]=='A' || m->raw[i+4]=='a') &&
681            ((m->raw[i+5]=='\r' && m->raw[i+6]=='\n') || m->raw[i+5]=='\n')){
682             /* Found it! */
683             i+=6;
684             if(m->raw[i-1]=='\r') i++;
685             m->pre_len=i;
686             m->msg=m->raw+i;
687             m->msg_len=m->raw_len-i;
688             break;
689         }
690     }
691     if(m->msg==NULL) return EX_DATAERR;
692
693     /* Find the end-of-DATA line */
694     prev='\n';
695     for(i=j=0; i<m->msg_len; i++){
696         if(prev=='\n' && m->msg[i]=='.'){
697             /* Dot at the beginning of a line */
698             if((m->msg[i+1]=='\r' && m->msg[i+2]=='\n') || m->msg[i+1]=='\n'){
699                 /* Lone dot! That's all, folks */
700                 m->post=m->msg+i;
701                 m->post_len=m->msg_len-i;
702                 m->msg_len=j;
703                 break;
704             } else if(m->msg[i+1]=='.'){
705                 /* Escaping dot, eliminate. */
706                 prev='.';
707                 continue;
708             } /* Else an ordinary dot, drop down to ordinary char handler */
709         }
710         prev=m->msg[i];
711         m->msg[j++]=m->msg[i];
712     }
713
714     m->type=MESSAGE_BSMTP;
715     m->out=m->msg;
716     m->out_len=m->msg_len;
717     return EX_OK;
718 }
719
720 int message_read(int fd, int flags, struct message *m){
721     libspamc_timeout = 0;
722
723     /* create the "private" part of the struct message */
724     m->priv = malloc (sizeof (struct libspamc_private_message));
725     if (m->priv == NULL) {
726         syslog(LOG_ERR, "message_read: malloc failed");
727         return EX_OSERR;
728     }
729     m->priv->flags = flags;
730
731     switch(flags&SPAMC_MODE_MASK){
732       case SPAMC_RAW_MODE:
733         return message_read_raw(fd, m);
734
735       case SPAMC_BSMTP_MODE:
736         return message_read_bsmtp(fd, m);
737
738       default:
739         syslog(LOG_ERR, "message_read: Unknown mode %d\n", flags&SPAMC_MODE_MASK);
740         return EX_USAGE;
741     }
742 }
743
744 long message_write(int fd, struct message *m){
745     long total=0;
746     off_t i, j;
747     off_t jlimit;
748     char buffer[1024];
749
750     if (m->priv->flags&SPAMC_CHECK_ONLY) {
751         if(m->is_spam==EX_ISSPAM || m->is_spam==EX_NOTSPAM){
752             return full_write(fd, m->out, m->out_len);
753
754         } else {
755             syslog(LOG_ERR, "oops! SPAMC_CHECK_ONLY is_spam: %d\n", m->is_spam);
756             return -1;
757         }
758     }
759
760     /* else we're not in CHECK_ONLY mode */
761     switch(m->type){
762       case MESSAGE_NONE:
763         syslog(LOG_ERR, "Cannot write this message, it's MESSAGE_NONE!\n");
764         return -1;
765
766       case MESSAGE_ERROR:
767         return full_write(fd, m->raw, m->raw_len);
768
769       case MESSAGE_RAW:
770         return full_write(fd, m->out, m->out_len);
771
772       case MESSAGE_BSMTP:
773         total=full_write(fd, m->pre, m->pre_len);
774         for(i=0; i<m->out_len; ){
775             jlimit = (off_t) (sizeof(buffer)/sizeof(*buffer)-4);
776             for(j=0; i < (off_t) m->out_len &&
777                                 j < jlimit;)
778             {
779                 if(i+1<m->out_len && m->out[i]=='\n' && m->out[i+1]=='.'){
780                     if (j > jlimit - 4) {
781                         break;          /* avoid overflow */
782                     }
783                     buffer[j++]=m->out[i++];
784                     buffer[j++]=m->out[i++];
785                     buffer[j++]='.';
786                 } else {
787                     buffer[j++]=m->out[i++];
788                 }
789             }
790             total+=full_write(fd, buffer, j);
791         }
792         return total+full_write(fd, m->post, m->post_len);
793
794       default:
795         syslog(LOG_ERR, "Unknown message type %d\n", m->type);
796         return -1;
797     }
798 }
799
800 void message_dump(int in_fd, int out_fd, struct message *m){
801     char buf[8196];
802     int bytes;
803     
804     if(m!=NULL && m->type!=MESSAGE_NONE) {
805         message_write(out_fd, m);
806     }
807     while((bytes=full_read(in_fd, buf, 8192, 8192))>0){
808         if (bytes!=full_write(out_fd, buf, bytes)) {
809             syslog(LOG_ERR, "oops! message_dump of %d returned different", bytes);
810         }
811     }
812 }
813
814 static int
815 _spamc_read_full_line (struct message *m, int flags, SSL *ssl, int sock,
816                 char *buf, int *lenp, int bufsiz)
817 {
818     int failureval;
819     int bytesread = 0;
820     int len;
821
822     UNUSED_VARIABLE(m);
823
824     /* Now, read from spamd */
825     for(len=0; len<bufsiz-1; len++) {
826         if(flags&SPAMC_USE_SSL) {
827           bytesread = ssl_timeout_read (ssl, buf+len, 1);
828         } else {
829           bytesread = fd_timeout_read (sock, buf+len, 1);
830         }
831
832         if(buf[len]=='\n') {
833             buf[len]='\0';
834             if (len > 0 && buf[len-1] == '\r') {
835                 len--;
836                 buf[len]='\0';
837             }
838             *lenp = len;
839             return EX_OK;
840         }
841
842         if(bytesread<=0){
843             failureval = EX_IOERR; goto failure;
844         }
845     }
846
847     syslog(LOG_ERR, "spamd responded with line of %d bytes, dying", len);
848     failureval = EX_TOOBIG;
849
850 failure:
851     return failureval;
852 }
853
854 /*
855  * May  7 2003 jm: using %f is bad where LC_NUMERIC is "," in the locale.
856  * work around using our own locale-independent float-parser code.
857  */
858 static float
859 _locale_safe_string_to_float (char *buf, int siz)
860 {
861   int is_neg;
862   char *cp, *dot;
863   int divider;
864   float ret, postdot;
865
866   buf[siz-1] = '\0';    /* ensure termination */
867
868   /* ok, let's illustrate using "100.033" as an example... */
869   
870   is_neg = 0;
871   if (*buf == '-') { is_neg = 1; }
872
873   ret = (float) (strtol (buf, &dot, 10));
874   if (dot == NULL) { return 0.0; }
875   if (dot != NULL && *dot != '.') { return ret; }
876
877   /* ex: ret == 100.0 */
878
879   cp = (dot + 1);
880   postdot = (float) (strtol (cp, NULL, 10));
881   if (postdot == 0.0) { return ret; }
882
883   /* ex: postdot == 33.0, cp="033" */
884
885   /* now count the number of decimal places and figure out what power of 10 to use */
886   divider = 1;
887   while (*cp != '\0') {
888     divider *= 10; cp++;
889   }
890
891   /* ex:
892    * cp="033", divider=1
893    * cp="33", divider=10
894    * cp="3", divider=100
895    * cp="", divider=1000
896    */
897
898   if (is_neg) {
899     ret -= (postdot / ((float) divider));
900   } else {
901     ret += (postdot / ((float) divider));
902   }
903   /* ex: ret == 100.033, tada! ... hopefully */
904
905   return ret;
906 }
907
908 static int
909 _handle_spamd_header (struct message *m, int flags, char *buf, int len)
910 {
911     char is_spam[6];
912     char s_str[21], t_str[21];
913
914     UNUSED_VARIABLE(len);
915
916     /* Feb 12 2003 jm: actually, I think sccanf is working fine here ;)
917      * let's stick with it for this parser.
918      * May  7 2003 jm: using %f is bad where LC_NUMERIC is "," in the locale.
919      * work around using our own locale-independent float-parser code.
920      */
921     if (sscanf(buf, "Spam: %5s ; %20s / %20s", is_spam, s_str, t_str) == 3)
922     {
923         m->score = _locale_safe_string_to_float (s_str, 20);
924         m->threshold = _locale_safe_string_to_float (t_str, 20);
925
926         /* Format is "Spam: x; y / x" */
927         m->is_spam=strcasecmp("true", is_spam) == 0 ? EX_ISSPAM: EX_NOTSPAM;
928
929         if(flags&SPAMC_CHECK_ONLY) {
930             m->out_len=snprintf (m->out, m->max_len+EXPANSION_ALLOWANCE,
931                         "%.1f/%.1f\n", m->score, m->threshold);
932         }
933         else if ((flags & SPAMC_REPORT_IFSPAM && m->is_spam == EX_ISSPAM)
934                 || (flags & SPAMC_REPORT))
935         {
936             m->out_len=snprintf (m->out, m->max_len+EXPANSION_ALLOWANCE,
937                         "%.1f/%.1f\n", m->score, m->threshold);
938         }
939         return EX_OK;
940
941     } else if(sscanf(buf, "Content-length: %d", &m->content_length) == 1) {
942         if (m->content_length < 0) {
943             syslog(LOG_ERR, "spamd responded with bad Content-length '%s'", buf);
944             return EX_PROTOCOL;
945         }
946         return EX_OK;
947     }
948
949     syslog(LOG_ERR, "spamd responded with bad header '%s'", buf);
950     return EX_PROTOCOL;
951 }
952
953 int message_filter(struct transport *tp, const char *username,
954                 int flags, struct message *m)
955 {
956     char buf[8192];
957     int bufsiz = (sizeof(buf) / sizeof(*buf)) - 4; /* bit of breathing room */
958     int len, i;
959     int sock = -1;
960     int rc;
961     char versbuf[20];
962     float version;
963     int response;
964     int failureval;
965     SSL_CTX* ctx;
966     SSL* ssl;
967     SSL_METHOD *meth;
968
969     if (flags&SPAMC_USE_SSL) {
970 #ifdef SPAMC_SSL
971       SSLeay_add_ssl_algorithms();
972       meth = SSLv2_client_method();
973       SSL_load_error_strings();
974       ctx = SSL_CTX_new(meth);
975 #else
976       UNUSED_VARIABLE(ssl);
977       UNUSED_VARIABLE(meth);
978       UNUSED_VARIABLE(ctx);
979       syslog(LOG_ERR, "spamc not built with SSL support");
980       return EX_SOFTWARE;
981 #endif
982     }    
983
984     m->is_spam=EX_TOOBIG;
985     if((m->out=malloc(m->max_len+EXPANSION_ALLOWANCE+1))==NULL){
986         failureval = EX_OSERR; goto failure;
987     }
988     m->out_len=0;
989
990
991     /* Build spamd protocol header */
992     if(flags & SPAMC_CHECK_ONLY) 
993       len=snprintf(buf, bufsiz, "CHECK %s\r\n", PROTOCOL_VERSION);
994     else if(flags & SPAMC_REPORT_IFSPAM)
995       len=snprintf(buf, bufsiz, "REPORT_IFSPAM %s\r\n", PROTOCOL_VERSION);
996     else if(flags & SPAMC_REPORT) 
997       len=snprintf(buf, bufsiz, "REPORT %s\r\n", PROTOCOL_VERSION);
998     else if(flags & SPAMC_SYMBOLS) 
999       len=snprintf(buf, bufsiz, "SYMBOLS %s\r\n", PROTOCOL_VERSION);
1000     else
1001       len=snprintf(buf, bufsiz, "PROCESS %s\r\n", PROTOCOL_VERSION);
1002
1003     if(len<0 || len >= bufsiz){ free(m->out); m->out=m->msg; m->out_len=m->msg_len; return EX_OSERR; }
1004     if(username!=NULL){
1005         len+=i=snprintf(buf+len, bufsiz-len, "User: %s\r\n", username);
1006         if(i<0 || len >= bufsiz){ free(m->out); m->out=m->msg; m->out_len=m->msg_len; return EX_OSERR; }
1007     }
1008     len+=i=snprintf(buf+len, bufsiz-len, "Content-length: %d\r\n", m->msg_len);
1009     if(i<0 || len >= bufsiz){ free(m->out); m->out=m->msg; m->out_len=m->msg_len; return EX_OSERR; }
1010     len+=i=snprintf(buf+len, bufsiz-len, "\r\n");
1011     if(i<0 || len >= bufsiz){ free(m->out); m->out=m->msg; m->out_len=m->msg_len; return EX_OSERR; }
1012
1013     libspamc_timeout = m->timeout;
1014
1015     if ( tp->socketpath )
1016         rc = try_to_connect_unix(tp, &sock);
1017     else
1018         rc = try_to_connect_tcp(tp, &sock);
1019
1020     if ( rc != EX_OK )
1021     {
1022         free(m->out); m->out=m->msg; m->out_len=m->msg_len;
1023         return i;
1024     }
1025
1026     if(flags&SPAMC_USE_SSL) {
1027 #ifdef SPAMC_SSL
1028       ssl = SSL_new(ctx);
1029       SSL_set_fd(ssl, sock);
1030       SSL_connect(ssl);
1031 #endif    
1032     }
1033
1034     /* Send to spamd */
1035     if(flags&SPAMC_USE_SSL) {
1036 #ifdef SPAMC_SSL
1037       SSL_write(ssl, buf, len);
1038       SSL_write(ssl, m->msg, m->msg_len);
1039 #endif
1040     } else {
1041       full_write(sock, buf, len);
1042       full_write(sock, m->msg, m->msg_len);
1043       shutdown(sock, SHUT_WR);
1044     }
1045
1046     /* ok, now read and parse it.  SPAMD/1.2 line first... */
1047     failureval = _spamc_read_full_line (m, flags, ssl, sock, buf, &len, bufsiz);
1048     if (failureval != EX_OK) { goto failure; }
1049
1050     if(sscanf(buf, "SPAMD/%18s %d %*s", versbuf, &response)!=2) {
1051         syslog(LOG_ERR, "spamd responded with bad string '%s'", buf);
1052         failureval = EX_PROTOCOL; goto failure;
1053     }
1054
1055     versbuf[19] = '\0';
1056     version = _locale_safe_string_to_float (versbuf, 20);
1057     if (version < 1.0) {
1058         syslog(LOG_ERR, "spamd responded with bad version string '%s'", versbuf);
1059         failureval = EX_PROTOCOL; goto failure;
1060     }
1061
1062     m->score = 0;
1063     m->threshold = 0;
1064     m->is_spam = EX_TOOBIG;
1065     while (1) {
1066         failureval = _spamc_read_full_line (m, flags, ssl, sock, buf, &len, bufsiz);
1067         if (failureval != EX_OK) { goto failure; }
1068
1069         if (len == 0 && buf[0] == '\0') {
1070             break;      /* end of headers */
1071         }
1072
1073         if (_handle_spamd_header(m, flags, buf, len) < 0) {
1074             failureval = EX_PROTOCOL; goto failure;
1075         }
1076     }
1077
1078     len = 0;            /* overwrite those headers */
1079
1080     if (flags&SPAMC_CHECK_ONLY) {
1081         close(sock); sock = -1;
1082         if (m->is_spam == EX_TOOBIG) {
1083               /* We should have gotten headers back... Damnit. */
1084               failureval = EX_PROTOCOL; goto failure;
1085         }
1086         return EX_OK;
1087     }
1088     else {
1089         if (m->content_length < 0) {
1090             /* should have got a length too. */
1091             failureval = EX_PROTOCOL; goto failure;
1092         }
1093
1094         /* have we already got something in the buffer (e.g. REPORT and
1095          * REPORT_IFSPAM both create a line from the "Spam:" hdr)?  If
1096          * so, add the size of that so our sanity check passes.
1097          */
1098         if (m->out_len > 0) {
1099             m->content_length += m->out_len;
1100         }
1101
1102         if (flags&SPAMC_USE_SSL) {
1103           len = full_read_ssl (ssl, (unsigned char *) m->out+m->out_len,
1104                      m->max_len+EXPANSION_ALLOWANCE+1-m->out_len,
1105                      m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
1106         } else{
1107           len = full_read (sock, m->out+m->out_len,
1108                      m->max_len+EXPANSION_ALLOWANCE+1-m->out_len,
1109                      m->max_len+EXPANSION_ALLOWANCE+1-m->out_len);
1110         }
1111
1112
1113         if(len+m->out_len>m->max_len+EXPANSION_ALLOWANCE){
1114             failureval = EX_TOOBIG; goto failure;
1115         }
1116         m->out_len+=len;
1117
1118         shutdown(sock, SHUT_RD);
1119         close(sock); sock = -1;
1120     }
1121     libspamc_timeout = 0;
1122
1123     if(m->out_len!=m->content_length) {
1124         syslog(LOG_ERR, "failed sanity check, %d bytes claimed, %d bytes seen",
1125                                 m->content_length, m->out_len);
1126         failureval = EX_PROTOCOL; goto failure;
1127     }
1128
1129     return EX_OK;
1130
1131 failure:
1132     free(m->out); m->out=m->msg; m->out_len=m->msg_len;
1133     if (sock != -1) {
1134       close(sock);
1135     }
1136     libspamc_timeout = 0;
1137
1138     if(flags&SPAMC_USE_SSL) {
1139 #ifdef SPAMC_SSL
1140       SSL_free(ssl);
1141       SSL_CTX_free(ctx);
1142 #endif
1143     }
1144     return failureval;
1145 }
1146
1147
1148 int message_process(struct transport *trans, char *username, int max_size, int in_fd, int out_fd, const int flags){
1149     int ret;
1150     struct message m;
1151
1152     m.type=MESSAGE_NONE;
1153
1154     m.max_len=max_size;
1155     ret=message_read(in_fd, flags, &m);
1156     if(ret!=EX_OK) goto FAIL;
1157     ret=message_filter(trans, username, flags, &m);
1158     if(ret!=EX_OK) goto FAIL;
1159     if(message_write(out_fd, &m)<0) goto FAIL;
1160     if(m.is_spam!=EX_TOOBIG) {
1161        message_cleanup(&m);
1162        return m.is_spam;
1163     }
1164     message_cleanup(&m);
1165     return ret;
1166
1167 FAIL:
1168    if(flags&SPAMC_CHECK_ONLY){
1169        full_write(out_fd, "0/0\n", 4);
1170        message_cleanup(&m);
1171        return EX_NOTSPAM;
1172    } else {
1173        message_dump(in_fd, out_fd, &m);
1174        message_cleanup(&m);
1175        return ret;
1176     }
1177 }
1178
1179 void message_cleanup(struct message *m) {
1180    if (m->out != NULL && m->out != m->raw) free(m->out);
1181    if (m->raw != NULL) free(m->raw);
1182    if (m->priv != NULL) free(m->priv);
1183    clear_message(m);
1184 }
1185
1186 /* Aug 14, 2002 bj: Obsolete! */
1187 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){
1188     int flags;
1189
1190     flags=SPAMC_RAW_MODE;
1191     if(my_check_only) flags|=SPAMC_CHECK_ONLY;
1192     if(my_safe_fallback) flags|=SPAMC_SAFE_FALLBACK;
1193
1194     return message_process(tp, username, max_size, in_fd, out_fd, flags);
1195 }
1196
1197 /*
1198  * init_transport()
1199  *
1200  *      Given a pointer to a transport structure, set it to "all empty".
1201  *      The default is a localhost connection.
1202  */
1203 void transport_init(struct transport *tp)
1204 {
1205         assert(tp != 0);
1206
1207         memset(tp, 0, sizeof *tp);
1208
1209         tp->type = TRANSPORT_LOCALHOST;
1210         tp->port = 783;
1211 }
1212
1213 /*
1214  * randomize_hosts()
1215  *
1216  *      Given the transport object that contains one or more IP addresses
1217  *      in this "hosts" list, rotate it by a random number of shifts to
1218  *      randomize them - this is a kind of load balancing. It's possible
1219  *      that the random number will be 0, which says not to touch. We don't
1220  *      do anything unless 
1221  */
1222
1223 static void randomize_hosts(struct transport *tp)
1224 {
1225 int     rnum;
1226
1227         assert(tp != 0);
1228
1229         if ( tp->nhosts <= 1 ) return;
1230
1231         rnum = rand() % tp->nhosts;
1232
1233         while ( rnum-- > 0 )
1234         {
1235         struct in_addr  tmp = tp->hosts[0];
1236         int             i;
1237
1238                 for (i = 1; i < tp->nhosts; i++ )
1239                         tp->hosts[i-1] = tp->hosts[i];
1240
1241                 tp->hosts[i-1] = tmp;
1242         }
1243 }
1244
1245 /*
1246  * transport_setup()
1247  *
1248  *      Given a "transport" object that says how we're to connect to the
1249  *      spam daemon, perform all the initial setup required to make the
1250  *      connection process a smooth one. The main work is to do the host
1251  *      name lookup and copy over all the IP addresses to make a local copy
1252  *      so they're not kept in the resolver's static state.
1253  *
1254  *      Here we also manage quasi-load balancing and failover: if we're
1255  *      doing load balancing, we randomly "rotate" the list to put it in
1256  *      a different order, and then if we're not doing failover we limit
1257  *      the hosts to just one. This way *all* connections are done with
1258  *      the intention of failover - makes the code a bit more clear.
1259  */
1260 int transport_setup(struct transport *tp, int flags)
1261 {
1262 struct hostent *hp = 0;
1263 char           **addrp;
1264
1265         assert(tp != 0);
1266
1267         switch ( tp->type )
1268         {
1269           case TRANSPORT_UNIX:
1270                 assert(tp->socketpath != 0);
1271                 return EX_OK;
1272
1273           case TRANSPORT_LOCALHOST:
1274                 tp->hosts[0].s_addr = inet_addr("127.0.0.1");
1275                 tp->nhosts          = 1;
1276                 return EX_OK;
1277
1278           case TRANSPORT_TCP:
1279                 if (NULL == (hp = gethostbyname(tp->hostname)))
1280                 {
1281                 int     origherr = h_errno;  /* take a copy before syslog() */
1282
1283                         syslog (LOG_ERR, "gethostbyname(%s) failed: h_errno=%d",
1284                                 tp->hostname, origherr);
1285                         switch (origherr)
1286                         {
1287                           case HOST_NOT_FOUND:
1288                           case NO_ADDRESS:
1289                           case NO_RECOVERY:
1290                                 return EX_NOHOST;
1291                           case TRY_AGAIN:
1292                                 return EX_TEMPFAIL;
1293                           default:
1294                                 return EX_OSERR;
1295                         }
1296                 }
1297
1298                 /*--------------------------------------------------------
1299                  * If we have no hosts at all, or if they are some other
1300                  * kind of address family besides IPv4, then we really
1301                  * just have no hosts at all.
1302                  */
1303                 if ( hp->h_addr_list[0] == 0 )
1304                 {
1305                         /* no hosts in this list */
1306                         return EX_NOHOST;
1307                 }
1308
1309                 if ( hp->h_length   != sizeof tp->hosts[0]
1310                   || hp->h_addrtype != AF_INET )
1311                 {
1312                         /* FAIL - bad size/protocol/family? */
1313                         return EX_NOHOST;
1314                 }
1315
1316                 /*--------------------------------------------------------
1317                  * Copy all the IP addresses into our private structure.
1318                  * This gets them out of the resolver's static area and
1319                  * means we won't ever walk all over the list with other
1320                  * calls.
1321                  */
1322                 tp->nhosts = 0;
1323
1324                 for (addrp = hp->h_addr_list; *addrp; addrp++)
1325                 {
1326                         if (tp->nhosts >= TRANSPORT_MAX_HOSTS-1) {
1327                                 syslog (LOG_ERR, "hit limit of %d hosts, ignoring remainder", TRANSPORT_MAX_HOSTS-1);
1328                                 break;
1329                         }
1330
1331                         memcpy(&tp->hosts[tp->nhosts], *addrp,
1332                                 sizeof tp->hosts[0]);
1333
1334                         tp->nhosts++;
1335                 }
1336
1337                 /*--------------------------------------------------------
1338                  * QUASI-LOAD-BALANCING
1339                  *
1340                  * If the user wants to do quasi load balancing, "rotate"
1341                  * the list by a random amount based on the current time.
1342                  * This may later be truncated to a single item. This is
1343                  * meaningful only if we have more than one host.
1344                  */
1345                 if ( (flags & SPAMC_RANDOMIZE_HOSTS)  &&  tp->nhosts > 1 )
1346                 {
1347                         randomize_hosts(tp);
1348                 }
1349
1350                 /*--------------------------------------------------------
1351                  * If the user wants no fallback, simply truncate the host
1352                  * list to just one - this pretends that this is the extent
1353                  * of our connection list - then it's not a special case.
1354                  */
1355                 if ( !(flags & SPAMC_SAFE_FALLBACK)  &&  tp->nhosts > 1 )
1356                 {
1357                         /* truncating list */
1358                         tp->nhosts = 1;
1359                 }
1360         }
1361         return EX_OK;
1362 }
1363
1364
1365 /* --------------------------------------------------------------------------- */
1366
1367 /*
1368  * Unit tests.  Must be built externally, e.g.:
1369  *
1370  * gcc -g -DLIBSPAMC_UNIT_TESTS spamd/spamc.c spamd/libspamc.c spamd/utils.c -o libspamctest
1371  * ./libspamctest
1372  *
1373  */
1374 #ifdef LIBSPAMC_UNIT_TESTS
1375
1376 static void
1377 _test_locale_safe_string_to_float_val (float input) {
1378   char inputstr[99], cmpbuf1[99], cmpbuf2[99];
1379   float output;
1380
1381   snprintf (inputstr, 99, "%f", input);
1382   output = _locale_safe_string_to_float (inputstr, 99);
1383   if (input == output) { return; }
1384
1385   /* could be a rounding error.  print as string and compare those */
1386   snprintf (cmpbuf1, 98, "%f", input);
1387   snprintf (cmpbuf2, 98, "%f", output);
1388   if (!strcmp (cmpbuf1, cmpbuf2)) { return; }
1389
1390   printf ("FAIL: input=%f != output=%f\n", input, output);
1391 }
1392
1393 static void
1394 unit_test_locale_safe_string_to_float (void) {
1395   float statictestset[] = {     /* will try both +ve and -ve */
1396         0.1, 0.01, 0.001, 0.0001, 0.00001, 0.000001,
1397         9.1, 9.91, 9.991, 9.9991, 9.99991, 9.999991,
1398         0.0             /* end of set constant */
1399   };
1400   float num;
1401   int i;
1402
1403   printf ("starting unit_test_locale_safe_string_to_float\n");
1404   /* tests of precision */
1405   for (i = 0; statictestset[i] != 0.0; i++) {
1406     _test_locale_safe_string_to_float_val (statictestset[i]);
1407     _test_locale_safe_string_to_float_val (-statictestset[i]);
1408     _test_locale_safe_string_to_float_val (1-statictestset[i]);
1409     _test_locale_safe_string_to_float_val (1+statictestset[i]);
1410   }
1411   /* now exhaustive, in steps of 0.01 */
1412   for (num = -1000.0; num < 1000.0; num += 0.01) {
1413     _test_locale_safe_string_to_float_val (num);
1414   }
1415   printf ("finished unit_test_locale_safe_string_to_float\n");
1416 }
1417
1418 void
1419 do_libspamc_unit_tests (void) {
1420   unit_test_locale_safe_string_to_float();
1421   exit (0);
1422 }
1423
1424 #endif /* LIBSPAMC_UNIT_TESTS */