chiark / gitweb /
prlimit.c: Fix format-string type mismatch.
[misc] / qmail-checkspam.c
1 /* -*-c-*-
2  *
3  * $Id: qmail-checkspam.c,v 1.2 2004/04/08 01:36:26 mdw Exp $
4  *
5  * Filter messages for spam
6  *
7  * (c) 2003 Mark Wooding
8  */
9
10 /*----- Licensing notice --------------------------------------------------*
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program; if not, write to the Free Software Foundation,
24  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25  */
26
27 /*----- Header files ------------------------------------------------------*/
28
29 #include <ctype.h>
30 #include <errno.h>
31 #include <limits.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35
36 #include <sys/types.h>
37 #include <sys/unistd.h>
38
39 #include <syslog.h>
40
41 #include <libspamc.h>
42
43 /*----- Main code ---------------------------------------------------------*/
44
45 static const char *strenv(const char *e, const char *d)
46 {
47   const char *p = getenv(e);
48   if (!p) return (d);
49   return (p);
50 }
51
52 static double dblenv(const char *e, double d)
53 {
54   const char *p = getenv(e);
55   char *q;
56   int err = errno;
57   double f;
58   if (!p) return (d);
59   errno = 0;
60   f = strtod(p, &q);
61   if (errno) {
62     syslog(LOG_ERR, "bad floating value `%s' for %s'; ignoring", p, e);
63     return (d);
64   }
65   errno = err;
66   return (f);
67 }
68
69 static int intenv(const char *e, int d)
70 {
71   const char *p = getenv(e);
72   char *q;
73   int err = errno;
74   long l;
75   if (!p) return (d);
76   errno = 0;
77   l = strtol(p, &q, 0);
78   if (errno) return (d);
79   errno = err;
80   if (l < 0 || l > INT_MAX) {
81     syslog(LOG_ERR, "bad integer value `%s' for `%s'; ignoring", p, e);
82     return (d);
83   }
84   return ((int)l);
85 }
86
87 static int safewrite(int fd, const void *p, size_t sz)
88 {
89   const char *pp = p;
90   ssize_t n;
91
92   while (sz > 0) {
93     n = write(fd, pp, sz);
94     if (sz <= 0 && errno != EINTR && errno != EAGAIN)
95       return (-1);
96     sz -= n;
97     pp += n;
98   }
99   return (0);
100 }
101
102 static int shovel(int from, int to, const char *what)
103 {
104   char buf[4096];
105   ssize_t n;
106
107   for (;;) {
108     n = read(from, buf, sizeof(buf));
109     if (n < 0 && errno != EINTR && errno != EAGAIN) {
110       syslog(LOG_ERR, "failed to read %s: %m", what);
111       return (-1);
112     }
113     else if (!n)
114       return (0);
115     if (safewrite(to, buf, n) < 0) {
116       syslog(LOG_ERR, "failed to write %s: %m", what);
117       return (-1);
118     }
119   }
120 }
121
122 #define ENVBUF 4096
123 #define NRCPT 16
124 struct envelope {
125   unsigned f;
126 #define EF_TRUNC 1u
127   char buf[ENVBUF + 1];
128   size_t n;
129   const char *send;
130   const char *rcpt[NRCPT];
131   size_t nrcpt;
132 };
133
134 static int readenv(struct envelope *e)
135 {
136   ssize_t n;
137   const char *p, *l;
138
139   /* Read the raw envelope data. */
140   e->n = 0;
141   e->f = 0;
142   while (e->n < ENVBUF) {
143     n = read(1, e->buf + e->n, ENVBUF - e->n);
144     if (n < 0 && errno != EINTR && errno != EAGAIN) {
145       syslog(LOG_ERR, "failed to read envelope: %m");
146       return (-1);
147     }
148     if (!n) break;
149     e->n += n;
150   }
151   e->buf[e->n] = 0;
152
153   /* Parse up the envelope data. */
154   p = e->buf; l = p + e->n;
155   e->nrcpt = 0;
156   if (*p++ != 'F') {
157     e->send = "<invalid>";
158     syslog(LOG_ERR, "corrupt envelope (no sender marker)");
159     return (-1);
160   }
161   e->send = p;
162   for (;;) {
163     if (p >= l) goto trunc;
164     else if (!*p++) break;
165   }
166
167   for (;;) {
168     if (p >= l) goto trunc;
169     if (!*p) break;
170     else if (*p++ != 'T') {
171       syslog(LOG_ERR, "corrupt envelope (no recipient marker)");
172       return (-1);
173     }
174     if (p >= l) goto trunc;
175     if (e->nrcpt < NRCPT) e->rcpt[e->nrcpt++] = p;
176     for (;;) {
177       if (p >= l) goto trunc;
178       else if (!*p++) break;
179     }
180   }
181   return (0);
182
183   /* Failed to reach the final terminator; maybe there's more.  This isn't a
184    * very bad situation.
185    */
186 trunc:
187   e->f |= EF_TRUNC;
188   return (0);
189 }
190
191 static void real(void)
192 {
193   const char *qmq =
194     strenv("QMAIL_CHECKSPAM_QUEUE", "/var/qmail/bin/qmail-queue");
195   execlp(qmq, qmq, (char *)0);
196   syslog(LOG_ERR, "failed to exec %s: %m", qmq);
197   exit(56);
198 }
199
200 static int split(int *fd_m, int *fd_e)
201 {
202   pid_t kid;
203   int pm[2], pe[2];
204
205   if (pipe(pm) || pipe(pe)) return (-1);
206   if ((kid = fork()) < 0) return (-1);
207   if (kid) {
208     dup2(pm[0], 0); close(pm[0]); close(pm[1]);
209     dup2(pe[0], 1); close(pe[0]); close(pe[1]);
210     real();
211   } else {
212     close(pm[0]); *fd_m = pm[1];
213     close(pe[0]); *fd_e = pe[1];
214   }
215   return (0);
216 }
217
218 static int writemsg(struct message *m, int fd_m, int shovelp)
219 {
220   if (message_write(fd_m, m) < 0) {
221     syslog(LOG_ERR, "failed to write message body: %m");
222     return (-1);
223   }
224   if (shovelp && shovel(0, fd_m, "message body"))
225     return (-1);
226   close(fd_m);
227   return (0);
228 }
229
230 static int writeenv(struct envelope *e, int fd_e)
231 {
232   if (safewrite(fd_e, e->buf, e->n) < 0) {
233     syslog(LOG_ERR, "failed to write envelope: %m");
234     return (-1);
235   }
236   if ((e->f & EF_TRUNC) && shovel(1, fd_e, "envelope"))
237     return (-1);
238   close(fd_e);
239   return (0);
240 }
241
242 static void logenv(struct envelope *e)
243 {
244   size_t i;
245
246   syslog(LOG_NOTICE, "sender = %s", e->send);
247   for (i = 0; i < e->nrcpt; i++)
248     syslog(LOG_NOTICE, "recipient %lu = %s", (unsigned long)i, e->rcpt[i]);
249 }
250
251 int main(int argc, char *argv[])
252 {
253   struct sockaddr sa;
254   struct message m;
255   struct envelope e;
256   int fd_m, fd_e;
257   const char *p;
258   double thresh;
259   int rc, port;
260
261   /* Set up logging output. */
262   if ((p = strrchr(argv[0], '/')) == 0) p = argv[0]; else p++;
263   openlog(p, LOG_PID, LOG_MAIL);
264
265   /* Read configuration from the environment. */
266   if (getenv("RELAYCLIENT")) real();
267   m.max_len = intenv("QMAIL_CHECKSPAM_MAXLEN", 2 * 1024 * 1024);
268   m.timeout = intenv("QMAIL_CHECKSPAM_TIMEOUT", 300);
269
270   /* Slurp an initial chunk of the message. */
271   rc = message_read(0, 0, &m);
272
273   switch (rc) {
274
275     case 0:
276       /* We read the message body OK.  Now try to slurp in the envelope and
277        * write a useful message.
278        */
279       if (readenv(&e)) return (54);
280       syslog(LOG_NOTICE, "scanning message");
281       logenv(&e);
282
283       /* Filter the message and see what happens. */
284       p = strenv("QMAIL_CHECKSPAM_SPAMDHOST", "localhost");
285       port = intenv("QMAIL_CHECKSPAM_SPAMDPORT", 783);
286       if (lookup_host(p, port, &sa)) {
287         syslog(LOG_ERR, "lookup failed for host `%s', port %d", p, port);
288         return (56);
289       }
290       if (message_filter(&sa, "spamd", 0, &m)) {
291         syslog(LOG_ERR, "filter failed unexpectedly");
292         return (74);
293       }
294
295       /* Check the resulting score. */
296       thresh = dblenv("QMAIL_CHECKSPAM_THRESH", m.threshold);
297       if (m.score >= thresh) {
298         syslog(LOG_NOTICE, "rejecting: score %g >= %g", m.score, thresh);
299         return (31);
300       }
301       syslog(LOG_NOTICE, "accepting: score %g < %g", m.score, thresh);
302
303       /* Send the remaining stuff on to the real server. */
304       if (split(&fd_m, &fd_e)) return (56);
305       if (writemsg(&m, fd_m, 0) || writeenv(&e, fd_e)) _exit(127);
306       break;
307
308     case EX_TOOBIG:
309       /* Message was too big.  We must pass it all on to the real server
310        * before picking up the envelope.
311        */
312       syslog(LOG_NOTICE, "message too large for filtering");
313       if (split(&fd_m, &fd_e)) return (56);
314       if (writemsg(&m, fd_m, 1) || readenv(&e)) _exit(127);
315       logenv(&e);
316       if (writeenv(&e, fd_e)) _exit(127);
317       break;
318
319     default:
320       syslog(LOG_ERR, "failed to read message (rc = %d): %m", rc);
321       return (54);
322   }
323
324   return (0);
325 }
326
327 /*----- That's all, folks -------------------------------------------------*/