Apply Ian Freislich's patch to fix a spamd timeout problem.
[exim.git] / src / src / spam.c
1 /* $Cambridge: exim/src/src/spam.c,v 1.5 2005/04/27 10:00:18 ph10 Exp $ */
2
3 /*************************************************
4 *     Exim - an Internet mail transport agent    *
5 *************************************************/
6
7 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-???? */
8 /* License: GPL */
9
10 /* Code for calling spamassassin's spamd. Called from acl.c. */
11
12 #include "exim.h"
13 #ifdef WITH_CONTENT_SCAN
14 #include "spam.h"
15
16 uschar spam_score_buffer[16];
17 uschar spam_score_int_buffer[16];
18 uschar spam_bar_buffer[128];
19 uschar spam_report_buffer[32600];
20 uschar prev_user_name[128] = "";
21 int spam_ok = 0;
22 int spam_rc = 0;
23
24 int spam(uschar **listptr) {
25   int sep = 0;
26   uschar *list = *listptr;
27   uschar *user_name;
28   uschar user_name_buffer[128];
29   unsigned long mbox_size;
30   FILE *mbox_file;
31   int spamd_sock;
32   uschar spamd_buffer[32600];
33   int i, j, offset, result;
34   uschar spamd_version[8];
35   uschar spamd_score_char;
36   double spamd_threshold, spamd_score;
37   int spamd_report_offset;
38   uschar *p,*q;
39   int override = 0;
40   time_t start;
41   size_t read, wrote;
42   struct sockaddr_un server;
43   struct pollfd pollfd;
44
45   /* find the username from the option list */
46   if ((user_name = string_nextinlist(&list, &sep,
47                                      user_name_buffer,
48                                      sizeof(user_name_buffer))) == NULL) {
49     /* no username given, this means no scanning should be done */
50     return FAIL;
51   };
52
53   /* if username is "0" or "false", do not scan */
54   if ( (Ustrcmp(user_name,"0") == 0) ||
55        (strcmpic(user_name,US"false") == 0) ) {
56     return FAIL;
57   };
58
59   /* if there is an additional option, check if it is "true" */
60   if (strcmpic(list,US"true") == 0) {
61     /* in that case, always return true later */
62     override = 1;
63   };
64
65   /* if we scanned for this username last time, just return */
66   if ( spam_ok && ( Ustrcmp(prev_user_name, user_name) == 0 ) ) {
67     if (override)
68       return OK;
69     else
70       return spam_rc;
71   };
72
73   /* make sure the eml mbox file is spooled up */
74   mbox_file = spool_mbox(&mbox_size);
75
76   if (mbox_file == NULL) {
77     /* error while spooling */
78     log_write(0, LOG_MAIN|LOG_PANIC,
79            "spam acl condition: error while creating mbox spool file");
80     return DEFER;
81   };
82
83   start = time(NULL);
84   /* socket does not start with '/' -> network socket */
85   if (*spamd_address != '/') {
86     time_t now = time(NULL);
87     int num_servers = 0;
88     int current_server = 0;
89     int start_server = 0;
90     uschar *address = NULL;
91     uschar *spamd_address_list_ptr = spamd_address;
92     uschar address_buffer[256];
93     spamd_address_container * spamd_address_vector[32];
94
95     /* Check how many spamd servers we have
96        and register their addresses */
97     while ((address = string_nextinlist(&spamd_address_list_ptr, &sep,
98                                         address_buffer,
99                                         sizeof(address_buffer))) != NULL) {
100
101       spamd_address_container *this_spamd =
102         (spamd_address_container *)store_get(sizeof(spamd_address_container));
103
104       /* grok spamd address and port */
105       if( sscanf(CS address, "%s %u", this_spamd->tcp_addr, &(this_spamd->tcp_port)) != 2 ) {
106         log_write(0, LOG_MAIN,
107           "spam acl condition: warning - invalid spamd address: '%s'", address);
108         continue;
109       };
110
111       spamd_address_vector[num_servers] = this_spamd;
112       num_servers++;
113       if (num_servers > 31)
114         break;
115     };
116
117     /* check if we have at least one server */
118     if (!num_servers) {
119       log_write(0, LOG_MAIN|LOG_PANIC,
120          "spam acl condition: no useable spamd server addresses in spamd_address configuration option.");
121       fclose(mbox_file);
122       return DEFER;
123     };
124
125     current_server = start_server = (int)now % num_servers;
126
127     while (1) {
128
129       debug_printf("trying server %s, port %u\n",
130                    spamd_address_vector[current_server]->tcp_addr,
131                    spamd_address_vector[current_server]->tcp_port);
132
133       /* contact a spamd */
134       if ( (spamd_sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
135         log_write(0, LOG_MAIN|LOG_PANIC,
136            "spam acl condition: error creating IP socket for spamd");
137         fclose(mbox_file);
138         return DEFER;
139       };
140
141       if (ip_connect( spamd_sock,
142                       AF_INET,
143                       spamd_address_vector[current_server]->tcp_addr,
144                       spamd_address_vector[current_server]->tcp_port,
145                       5 ) > -1) {
146         /* connection OK */
147         break;
148       };
149
150       log_write(0, LOG_MAIN|LOG_PANIC,
151          "spam acl condition: warning - spamd connection to %s, port %u failed: %s",
152          spamd_address_vector[current_server]->tcp_addr,
153          spamd_address_vector[current_server]->tcp_port,
154          strerror(errno));
155       current_server++;
156       if (current_server >= num_servers)
157         current_server = 0;
158       if (current_server == start_server) {
159         log_write(0, LOG_MAIN|LOG_PANIC, "spam acl condition: all spamd servers failed");
160         fclose(mbox_file);
161         close(spamd_sock);
162         return DEFER;
163       };
164     };
165
166   }
167   else {
168     /* open the local socket */
169
170     if ((spamd_sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
171       log_write(0, LOG_MAIN|LOG_PANIC,
172                 "malware acl condition: spamd: unable to acquire socket (%s)",
173                 strerror(errno));
174       fclose(mbox_file);
175       return DEFER;
176     }
177
178     server.sun_family = AF_UNIX;
179     Ustrcpy(server.sun_path, spamd_address);
180
181     if (connect(spamd_sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
182       log_write(0, LOG_MAIN|LOG_PANIC,
183                 "malware acl condition: spamd: unable to connect to UNIX socket %s (%s)",
184                 spamd_address, strerror(errno) );
185       fclose(mbox_file);
186       close(spamd_sock);
187       return DEFER;
188     }
189
190   }
191
192   /* now we are connected to spamd on spamd_sock */
193   snprintf(CS spamd_buffer,
194            sizeof(spamd_buffer),
195            "REPORT SPAMC/1.2\r\nUser: %s\r\nContent-length: %ld\r\n\r\n",
196            user_name,
197            mbox_size);
198
199   /* send our request */
200   if (send(spamd_sock, spamd_buffer, Ustrlen(spamd_buffer), 0) < 0) {
201     close(spamd_sock);
202     log_write(0, LOG_MAIN|LOG_PANIC,
203          "spam acl condition: spamd send failed: %s", strerror(errno));
204     fclose(mbox_file);
205     close(spamd_sock);
206     return DEFER;
207   };
208
209   /* now send the file */
210   /* spamd sometimes accepts conections but doesn't read data off
211    * the connection.  We make the file descriptor non-blocking so
212    * that the write will only write sufficient data without blocking
213    * and we poll the desciptor to make sure that we can write without
214    * blocking.  Short writes are gracefully handled and if the whole
215    * trasaction takes too long it is aborted.
216    */
217   pollfd.fd = spamd_sock;
218   pollfd.events = POLLOUT;
219   fcntl(spamd_sock, F_SETFL, O_NONBLOCK);
220   do {
221     read = fread(spamd_buffer,1,sizeof(spamd_buffer),mbox_file);
222     if (read > 0) {
223       offset = 0;
224 again:
225       result = poll(&pollfd, 1, 1000);
226       if (result == -1 && errno == EINTR)
227         continue;
228       else if (result < 1) {
229         if (result == -1)
230           log_write(0, LOG_MAIN|LOG_PANIC,
231             "spam acl condition: %s on spamd socket", strerror(errno));
232         else {
233           if (time(NULL) - start < SPAMD_TIMEOUT)
234           goto again;
235           log_write(0, LOG_MAIN|LOG_PANIC,
236             "spam acl condition: timed out writing spamd socket");
237         }
238         close(spamd_sock);
239         fclose(mbox_file);
240         return DEFER;
241       }
242       wrote = send(spamd_sock,spamd_buffer + offset,read - offset,0);
243       if (offset + wrote != read) {
244         offset += wrote;
245         goto again;
246       }
247     }
248   }
249   while (!feof(mbox_file) && !ferror(mbox_file));
250   if (ferror(mbox_file)) {
251     log_write(0, LOG_MAIN|LOG_PANIC,
252       "spam acl condition: error reading spool file: %s", strerror(errno));
253     close(spamd_sock);
254     fclose(mbox_file);
255     return DEFER;
256   }
257
258   fclose(mbox_file);
259
260   /* we're done sending, close socket for writing */
261   shutdown(spamd_sock,SHUT_WR);
262
263   /* read spamd response using what's left of the timeout.
264    */
265   memset(spamd_buffer, 0, sizeof(spamd_buffer));
266   offset = 0;
267   while((i = ip_recv(spamd_sock,
268                      spamd_buffer + offset,
269                      sizeof(spamd_buffer) - offset - 1,
270                      SPAMD_TIMEOUT - time(NULL) + start)) > 0 ) {
271     offset += i;
272   }
273
274   /* error handling */
275   if((i <= 0) && (errno != 0)) {
276     log_write(0, LOG_MAIN|LOG_PANIC,
277          "spam acl condition: error reading from spamd socket: %s", strerror(errno));
278     close(spamd_sock);
279     return DEFER;
280   }
281
282   /* reading done */
283   close(spamd_sock);
284
285   /* dig in the spamd output and put the report in a multiline header, if requested */
286   if( sscanf(CS spamd_buffer,"SPAMD/%s 0 EX_OK\r\nContent-length: %*u\r\n\r\n%lf/%lf\r\n%n",
287              spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
288
289     /* try to fall back to pre-2.50 spamd output */
290     if( sscanf(CS spamd_buffer,"SPAMD/%s 0 EX_OK\r\nSpam: %*s ; %lf / %lf\r\n\r\n%n",
291                spamd_version,&spamd_score,&spamd_threshold,&spamd_report_offset) != 3 ) {
292       log_write(0, LOG_MAIN|LOG_PANIC,
293          "spam acl condition: cannot parse spamd output");
294       return DEFER;
295     };
296   };
297
298   /* Create report. Since this is a multiline string,
299   we must hack it into shape first */
300   p = &spamd_buffer[spamd_report_offset];
301   q = spam_report_buffer;
302   while (*p != '\0') {
303     /* skip \r */
304     if (*p == '\r') {
305       p++;
306       continue;
307     };
308     *q = *p;
309     q++;
310     if (*p == '\n') {
311       *q = '\t';
312       q++;
313       /* eat whitespace */
314       while( (*p <= ' ') && (*p != '\0') ) {
315         p++;
316       };
317       p--;
318     };
319     p++;
320   };
321   /* NULL-terminate */
322   *q = '\0';
323   q--;
324   /* cut off trailing leftovers */
325   while (*q <= ' ') {
326     *q = '\0';
327     q--;
328   };
329   spam_report = spam_report_buffer;
330
331   /* create spam bar */
332   spamd_score_char = spamd_score > 0 ? '+' : '-';
333   j = abs((int)(spamd_score));
334   i = 0;
335   if( j != 0 ) {
336     while((i < j) && (i <= MAX_SPAM_BAR_CHARS))
337        spam_bar_buffer[i++] = spamd_score_char;
338   }
339   else{
340     spam_bar_buffer[0] = '/';
341     i = 1;
342   }
343   spam_bar_buffer[i] = '\0';
344   spam_bar = spam_bar_buffer;
345
346   /* create "float" spam score */
347   snprintf(CS spam_score_buffer, sizeof(spam_score_buffer),"%.1f", spamd_score);
348   spam_score = spam_score_buffer;
349
350   /* create "int" spam score */
351   j = (int)((spamd_score + 0.001)*10);
352   snprintf(CS spam_score_int_buffer, sizeof(spam_score_int_buffer), "%d", j);
353   spam_score_int = spam_score_int_buffer;
354
355   /* compare threshold against score */
356   if (spamd_score >= spamd_threshold) {
357     /* spam as determined by user's threshold */
358     spam_rc = OK;
359   }
360   else {
361     /* not spam */
362     spam_rc = FAIL;
363   };
364
365   /* remember user name and "been here" for it */
366   Ustrcpy(prev_user_name, user_name);
367   spam_ok = 1;
368
369   if (override) {
370     /* always return OK, no matter what the score */
371     return OK;
372   }
373   else {
374     return spam_rc;
375   };
376 }
377
378 #endif